diff --git a/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml b/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml index 4dcc60b75..55a6521fe 100644 --- a/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml +++ b/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml @@ -363,324 +363,330 @@ parentNoteId TEXT|0s NULL - + + 5 + TEXT|0s + 1 + "" + + 1 noteContentId 1 - + noteId - + noteContentId 1 sqlite_autoindex_note_contents_1 - + 1 TEXT|0s 1 - + 2 TEXT|0s 1 - + 3 TEXT|0s - + 4 TEXT|0s - + 5 INT|0s 1 0 - + 6 TEXT|0s 1 - + 7 TEXT|0s 1 - + 8 TEXT|0s 1 '' - + 9 TEXT|0s 1 '' - + 10 TEXT|0s 1 "" - + 1 noteRevisionId 1 - + noteId - + dateModifiedFrom - + dateModifiedTo - + noteRevisionId 1 sqlite_autoindex_note_revisions_1 - + 1 TEXT|0s 1 - + 2 TEXT|0s 1 "note" - + 3 INT|0s 1 0 - + 4 TEXT|0s 1 'text' - + 5 TEXT|0s 1 'text/html' - + 6 TEXT|0s 1 "" - + 7 INT|0s 1 0 - + 8 TEXT|0s 1 - + 9 TEXT|0s 1 - + 1 noteId 1 - + isDeleted - + noteId 1 sqlite_autoindex_notes_1 - + 1 TEXT|0s 1 - + 2 TEXT|0s - + 3 INT|0s - + 4 INTEGER|0s 1 0 - + 5 TEXT|0s 1 "" - + 6 TEXT|0s 1 '1970-01-01T00:00:00.000Z' - + 1 name 1 - + name 1 sqlite_autoindex_options_1 - + 1 TEXT|0s 1 - + 2 TEXT|0s 1 - + 3 TEXT|0s 1 "" - + 4 TEXT|0s 1 - + 5 INT|0s - + 1 branchId 1 - + branchId 1 sqlite_autoindex_recent_notes_1 - + 1 TEXT|0s 1 - + 2 TEXT|0s 1 - + 1 sourceId 1 - + sourceId 1 sqlite_autoindex_source_ids_1 - + 1 text|0s - + 2 text|0s - + 3 text|0s - + 4 integer|0s - + 5 text|0s - + 1 - + 2 - + 1 INTEGER|0s 1 1 - + 2 TEXT|0s 1 - + 3 TEXT|0s 1 - + 4 TEXT|0s 1 - + 5 TEXT|0s 1 - + entityName entityId 1 - + syncDate - + id 1 diff --git a/db/migrations/0125__create_note_content_table.sql b/db/migrations/0125__create_note_content_table.sql index 1bba63b5f..ef5ebd4fc 100644 --- a/db/migrations/0125__create_note_content_table.sql +++ b/db/migrations/0125__create_note_content_table.sql @@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS "note_contents" ( `noteId` TEXT NOT NULL, `isProtected` INT NOT NULL DEFAULT 0, `content` TEXT NULL DEFAULT NULL, + `hash` TEXT DEFAULT "" NOT NULL, PRIMARY KEY(`noteContentId`) ); diff --git a/src/entities/note.js b/src/entities/note.js index 914aec87d..bc4937cdd 100644 --- a/src/entities/note.js +++ b/src/entities/note.js @@ -67,6 +67,13 @@ class Note extends Entity { return this.noteContent; } + /** @returns {Promise<*>} */ + async getContent() { + const noteContent = await this.getNoteContent(); + + return noteContent.content; + } + /** @returns {boolean} true if this note is the root of the note tree. Root note has "root" noteId */ isRoot() { return this.noteId === 'root'; @@ -606,10 +613,6 @@ class Note extends Entity { } beforeSaving() { - if (this.isJson() && this.jsonContent) { - this.content = JSON.stringify(this.jsonContent, null, '\t'); - } - // we do this here because encryption needs the note ID for the IV this.generateIdIfNecessary(); @@ -637,7 +640,6 @@ class Note extends Entity { else { // updating protected note outside of protected session means we will keep original ciphertexts pojo.title = pojo.titleCipherText; - pojo.content = pojo.contentCipherText; } } @@ -645,7 +647,6 @@ class Note extends Entity { delete pojo.isContentAvailable; delete pojo.__attributeCache; delete pojo.titleCipherText; - delete pojo.contentCipherText; } } diff --git a/src/public/javascripts/dialogs/note_source.js b/src/public/javascripts/dialogs/note_source.js index e04171d0d..b91cb0165 100644 --- a/src/public/javascripts/dialogs/note_source.js +++ b/src/public/javascripts/dialogs/note_source.js @@ -8,7 +8,7 @@ function showDialog() { $dialog.modal(); - const noteText = noteDetailService.getCurrentNote().content; + const noteText = noteDetailService.getCurrentNote().noteContent.content; $noteSource.text(formatHtml(noteText)); } diff --git a/src/public/javascripts/services/note_detail.js b/src/public/javascripts/services/note_detail.js index 3f47ab698..77031cbee 100644 --- a/src/public/javascripts/services/note_detail.js +++ b/src/public/javascripts/services/note_detail.js @@ -117,7 +117,7 @@ async function saveNote() { } note.title = $noteTitle.val(); - note.content = getCurrentNoteContent(note); + note.noteContent.content = getCurrentNoteContent(note); // it's important to set the flag back to false immediatelly after retrieving title and content // otherwise we might overwrite another change (especially async code) diff --git a/src/public/javascripts/services/note_tooltip.js b/src/public/javascripts/services/note_tooltip.js index c3f6caccb..9590fb8b7 100644 --- a/src/public/javascripts/services/note_tooltip.js +++ b/src/public/javascripts/services/note_tooltip.js @@ -111,13 +111,13 @@ async function renderTooltip(note, attributes) { } if (note.type === 'text') { - // surround with
for a case when note.content is pure text (e.g. "[protected]") which + // surround with
for a case when note's content is pure text (e.g. "[protected]") which // then fails the jquery non-empty text test - content += '
' + note.content + '
'; + content += '
' + note.noteContent.content + '
'; } else if (note.type === 'code') { content += $("
")
-            .text(note.content)
+            .text(note.noteContent.content)
             .prop('outerHTML');
     }
     else if (note.type === 'image') {
diff --git a/src/routes/api/file_upload.js b/src/routes/api/file_upload.js
index 73620b865..663432b2f 100644
--- a/src/routes/api/file_upload.js
+++ b/src/routes/api/file_upload.js
@@ -51,7 +51,7 @@ async function downloadNoteFile(noteId, res) {
     res.setHeader('Content-Disposition', utils.getContentDisposition(fileName));
     res.setHeader('Content-Type', note.mime);
 
-    res.send(note.content);
+    res.send((await note.getNoteContent()).content);
 }
 
 async function downloadFile(req, res) {
diff --git a/src/routes/api/image.js b/src/routes/api/image.js
index 15b6236d9..f1f2ec8b8 100644
--- a/src/routes/api/image.js
+++ b/src/routes/api/image.js
@@ -21,7 +21,7 @@ async function returnImage(req, res) {
 
     res.set('Content-Type', image.mime);
 
-    res.send(image.content);
+    res.send((await note.getNoteContent()).content);
 }
 
 async function uploadImage(req) {
diff --git a/src/services/export/opml.js b/src/services/export/opml.js
index b00ed9340..5a9f164ed 100644
--- a/src/services/export/opml.js
+++ b/src/services/export/opml.js
@@ -17,7 +17,7 @@ async function exportToOpml(branch, res) {
         const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title;
 
         const preparedTitle = prepareText(title);
-        const preparedContent = prepareText(note.content);
+        const preparedContent = prepareText(await note.getContent());
 
         res.write(`\n`);
 
diff --git a/src/services/export/single.js b/src/services/export/single.js
index c6cc979dd..e5f2b833e 100644
--- a/src/services/export/single.js
+++ b/src/services/export/single.js
@@ -18,30 +18,32 @@ async function exportSingleNote(branch, format, res) {
 
     let payload, extension, mime;
 
+    const noteContent = await note.getNoteContent();
+
     if (note.type === 'text') {
         if (format === 'html') {
-            if (!note.content.toLowerCase().includes("';
+            if (!noteContent.content.toLowerCase().includes("';
             }
 
-            payload = html.prettyPrint(note.content, {indent_size: 2});
+            payload = html.prettyPrint(noteContent.content, {indent_size: 2});
             extension = 'html';
             mime = 'text/html';
         }
         else if (format === 'markdown') {
             const turndownService = new TurndownService();
-            payload = turndownService.turndown(note.content);
+            payload = turndownService.turndown(noteContent.content);
             extension = 'md';
             mime = 'text/markdown'
         }
     }
     else if (note.type === 'code') {
-        payload = note.content;
+        payload = noteContent.content;
         extension = mimeTypes.extension(note.mime) || 'code';
         mime = note.mime;
     }
     else if (note.type === 'relation-map' || note.type === 'search') {
-        payload = note.content;
+        payload = noteContent.content;
         extension = 'json';
         mime = 'application/json';
     }
diff --git a/src/services/export/tar.js b/src/services/export/tar.js
index 9a42aedfb..7634f3cbb 100644
--- a/src/services/export/tar.js
+++ b/src/services/export/tar.js
@@ -123,7 +123,7 @@ async function exportToTar(branch, format, res) {
         const childBranches = await note.getChildBranches();
 
         // if it's a leaf then we'll export it even if it's empty
-        if (note.content.length > 0 || childBranches.length === 0) {
+        if ((await note.getContent()).length > 0 || childBranches.length === 0) {
             meta.dataFileName = getDataFileName(note, baseFileName, existingFileNames);
         }
 
@@ -147,19 +147,21 @@ async function exportToTar(branch, format, res) {
         return meta;
     }
 
-    function prepareContent(note, format) {
+    async function prepareContent(note, format) {
+        const content = await note.getContent();
+
         if (format === 'html') {
-            if (!note.content.toLowerCase().includes("';
+            if (!content.toLowerCase().includes("';
             }
 
-            return html.prettyPrint(note.content, {indent_size: 2});
+            return html.prettyPrint(content, {indent_size: 2});
         }
         else if (format === 'markdown') {
-            return turndownService.turndown(note.content);
+            return turndownService.turndown(content);
         }
         else {
-            return note.content;
+            return content;
         }
     }
 
@@ -179,7 +181,7 @@ async function exportToTar(branch, format, res) {
         notePaths[note.noteId] = path + (noteMeta.dataFileName || noteMeta.dirFileName);
 
         if (noteMeta.dataFileName) {
-            const content = prepareContent(note, noteMeta.format);
+            const content = await prepareContent(note, noteMeta.format);
 
             pack.entry({name: path + noteMeta.dataFileName, size: content.length}, content);
         }
diff --git a/src/services/import/enex.js b/src/services/import/enex.js
index 183e2b192..5793a1d05 100644
--- a/src/services/import/enex.js
+++ b/src/services/import/enex.js
@@ -218,6 +218,8 @@ async function importEnex(file, parentNote) {
             mime: 'text/html'
         })).note;
 
+        const noteContent = await noteEntity.getNoteContent();
+
         for (const resource of resources) {
             const hash = utils.md5(resource.content);
 
@@ -238,8 +240,8 @@ async function importEnex(file, parentNote) {
 
               const resourceLink = `${utils.escapeHtml(resource.title)}`;
 
-              noteEntity.content = noteEntity.content.replace(mediaRegex, resourceLink);
-            }
+              noteContent.content = noteContent.content.replace(mediaRegex, resourceLink);
+            };
 
             if (["image/jpeg", "image/png", "image/gif"].includes(resource.mime)) {
               try {
@@ -249,12 +251,12 @@ async function importEnex(file, parentNote) {
 
                 const imageLink = ``;
 
-                noteEntity.content = noteEntity.content.replace(mediaRegex, imageLink);
+                noteContent.content = noteContent.content.replace(mediaRegex, imageLink);
 
-                if (!note.content.includes(imageLink)) {
+                if (!noteContent.content.includes(imageLink)) {
                     // if there wasn't any match for the reference, we'll add the image anyway
                     // otherwise image would be removed since no note would include it
-                    note.content += imageLink;
+                    noteContent.content += imageLink;
                 }
               } catch (e) {
                 log.error("error when saving image from ENEX file: " + e);
@@ -267,7 +269,7 @@ async function importEnex(file, parentNote) {
         }
 
         // save updated content with links to files/images
-        await noteEntity.save();
+        await noteContent.save();
     }
 
     saxStream.on("closetag", async tag => {
diff --git a/src/services/import/tar.js b/src/services/import/tar.js
index 8170274a4..b631bbeb8 100644
--- a/src/services/import/tar.js
+++ b/src/services/import/tar.js
@@ -245,8 +245,10 @@ async function importTar(fileBuffer, importRootNote) {
         let note = await repository.getNote(noteId);
 
         if (note) {
-            note.content = content;
-            await note.save();
+            const noteContent = await note.getNoteContent();
+
+            noteContent.content = content;
+            await noteContent.save();
         }
         else {
             const noteTitle = getNoteTitle(filePath, noteMeta);
diff --git a/src/services/notes.js b/src/services/notes.js
index 7cebb2dee..9e07260e0 100644
--- a/src/services/notes.js
+++ b/src/services/notes.js
@@ -8,6 +8,7 @@ const eventService = require('./events');
 const repository = require('./repository');
 const cls = require('../services/cls');
 const Note = require('../entities/note');
+const NoteContent = require('../entities/note_content');
 const Link = require('../entities/link');
 const NoteRevision = require('../entities/note_revision');
 const Branch = require('../entities/branch');
@@ -87,12 +88,16 @@ async function createNewNote(parentNoteId, noteData) {
     const note = await new Note({
         noteId: noteData.noteId, // optionally can force specific noteId
         title: noteData.title,
-        content: noteData.content,
         isProtected: noteData.isProtected,
         type: noteData.type || 'text',
         mime: noteData.mime || 'text/html'
     }).save();
 
+    note.noteContent = await new NoteContent({
+        noteId: note.noteId,
+        content: noteData.content
+    });
+
     const branch = await new Branch({
         noteId: note.noteId,
         parentNoteId: parentNoteId,
@@ -284,6 +289,12 @@ async function saveLinks(note, content) {
 }
 
 async function saveNoteRevision(note) {
+    // files and images are immutable, they can't be updated
+    // but we don't even version titles which is probably not correct
+    if (note.type !== 'file' || note.type !== 'image' || await note.hasLabel('disableVersioning')) {
+        return;
+    }
+
     const now = new Date();
     const noteRevisionSnapshotTimeInterval = parseInt(await optionService.getOption('noteRevisionSnapshotTimeInterval'));
 
@@ -294,16 +305,12 @@ async function saveNoteRevision(note) {
 
     const msSinceDateCreated = now.getTime() - dateUtils.parseDateTime(note.dateCreated).getTime();
 
-    if (note.type !== 'file'
-        && !await note.hasLabel('disableVersioning')
-        && !existingNoteRevisionId
-        && msSinceDateCreated >= noteRevisionSnapshotTimeInterval * 1000) {
-
+    if (!existingNoteRevisionId && msSinceDateCreated >= noteRevisionSnapshotTimeInterval * 1000) {
         await new NoteRevision({
             noteId: note.noteId,
             // title and text should be decrypted now
             title: note.title,
-            content: note.content,
+            content: note.noteContent.content,
             type: note.type,
             mime: note.mime,
             isProtected: false, // will be fixed in the protectNoteRevisions() call
@@ -320,22 +327,23 @@ async function updateNote(noteId, noteUpdates) {
         throw new Error(`Note ${noteId} is not available for change!`);
     }
 
-    if (note.type === 'file' || note.type === 'image') {
-        // files and images are immutable, they can't be updated
-        noteUpdates.content = note.content;
-    }
-
     await saveNoteRevision(note);
 
     const noteTitleChanged = note.title !== noteUpdates.title;
 
-    noteUpdates.content = await saveLinks(note, noteUpdates.content);
+    noteUpdates.noteContent.content = await saveLinks(note, noteUpdates.noteContent.content);
 
     note.title = noteUpdates.title;
-    note.setContent(noteUpdates.content);
     note.isProtected = noteUpdates.isProtected;
     await note.save();
 
+    if (note.type !== 'file' && note.type !== 'image') {
+        const noteContent = await note.getNoteContent();
+        noteContent.content = noteUpdates.noteContent.content;
+        noteContent.isProtected = noteUpdates.isProtected;
+        await noteContent.save();
+    }
+
     if (noteTitleChanged) {
         await triggerNoteTitleChanged(note);
     }
@@ -394,7 +402,7 @@ async function cleanupDeletedNotes() {
     // it's better to not use repository for this because it will complain about saving protected notes
     // out of protected session
 
-    await sql.execute("UPDATE notes SET content = NULL WHERE isDeleted = 1 AND content IS NOT NULL AND dateModified <= ?", [dateUtils.dateStr(cutoffDate)]);
+    await sql.execute("UPDATE note_contents SET content = NULL WHERE content IS NOT NULL AND noteId IN (SELECT noteId FROM notes WHERE isDeleted = 1 AND notes.dateModified <= ?)", [dateUtils.dateStr(cutoffDate)]);
 
     await sql.execute("UPDATE note_revisions SET content = NULL WHERE note_revisions.content IS NOT NULL AND noteId IN (SELECT noteId FROM notes WHERE isDeleted = 1 AND notes.dateModified <= ?)", [dateUtils.dateStr(cutoffDate)]);
 }
diff --git a/src/services/repository.js b/src/services/repository.js
index e6cb4fc27..967bdd19d 100644
--- a/src/services/repository.js
+++ b/src/services/repository.js
@@ -42,6 +42,14 @@ async function getNote(noteId) {
     return await getEntity("SELECT * FROM notes WHERE noteId = ?", [noteId]);
 }
 
+/** @returns {Promise} */
+async function getNoteWithContent(noteId) {
+    const note = await getEntity("SELECT * FROM notes WHERE noteId = ?", [noteId]);
+    await note.getNoteContent();
+
+    return note;
+}
+
 /** @returns {Promise} */
 async function getNoteContent(noteContentId) {
     return await getEntity("SELECT * FROM note_contents WHERE noteContentId = ?", [noteContentId]);
@@ -126,6 +134,7 @@ module.exports = {
     getEntities,
     getEntity,
     getNote,
+    getNoteWithContent,
     getNoteContent,
     getBranch,
     getAttribute,
diff --git a/src/services/script.js b/src/services/script.js
index 596213a84..11c96af3e 100644
--- a/src/services/script.js
+++ b/src/services/script.js
@@ -56,10 +56,10 @@ async function executeBundle(bundle, apiParams = {}) {
  */
 async function executeScript(script, params, startNoteId, currentNoteId, originEntityName, originEntityId) {
     const startNote = await repository.getNote(startNoteId);
-    const currentNote = await repository.getNote(currentNoteId);
+    const currentNote = await repository.getNoteWithContent(currentNoteId);
     const originEntity = await repository.getEntityFromName(originEntityName, originEntityId);
 
-    currentNote.content = `return await (${script}\r\n)(${getParams(params)})`;
+    currentNote.noteContent.content = `return await (${script}\r\n)(${getParams(params)})`;
     currentNote.type = 'code';
     currentNote.mime = 'application/javascript;env=backend';
 
@@ -158,7 +158,7 @@ apiContext.modules['${note.noteId}'] = {};
 ${root ? 'return ' : ''}await ((async function(exports, module, require, api` + (modules.length > 0 ? ', ' : '') +
             modules.map(child => sanitizeVariableName(child.title)).join(', ') + `) {
 try {
-${note.content};
+${await note.getContent()};
 } catch (e) { throw new Error("Load of script note \\"${note.title}\\" (${note.noteId}) failed with: " + e.message); }
 if (!module.exports) module.exports = {};
 for (const exportKey in exports) module.exports[exportKey] = exports[exportKey];
@@ -167,7 +167,7 @@ for (const exportKey in exports) module.exports[exportKey] = exports[exportKey];
 `;
     }
     else if (note.isHtml()) {
-        bundle.html += note.content;
+        bundle.html += await note.getContent();
     }
 
     return bundle;
diff --git a/src/services/sync_update.js b/src/services/sync_update.js
index 630f95ebc..a27304097 100644
--- a/src/services/sync_update.js
+++ b/src/services/sync_update.js
@@ -48,14 +48,16 @@ async function updateEntity(sync, entity, sourceId) {
     }
 }
 
-function deserializeNoteContentBuffer(note) {
-    if (note.content !== null && (note.type === 'file' || note.type === 'image')) {
-        note.content = Buffer.from(note.content, 'base64');
+async function deserializeNoteContentBuffer(note) {
+    const noteContent = await note.getNoteContent();
+
+    if (noteContent.content !== null && (note.type === 'file' || note.type === 'image')) {
+        noteContent.content = Buffer.from(noteContent.content, 'base64');
     }
 }
 
 async function updateNote(entity, sourceId) {
-    deserializeNoteContentBuffer(entity);
+    await deserializeNoteContentBuffer(entity);
 
     const origNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [entity.noteId]);