mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	note ancillary/attachment backported from dev branch
This commit is contained in:
		| @@ -4,6 +4,7 @@ UPDATE notes SET title = 'title' WHERE noteId != 'root' AND noteId NOT LIKE '\_% | ||||
| UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL; | ||||
| UPDATE note_revisions SET title = 'title'; | ||||
| UPDATE note_revision_contents SET content = 'text' WHERE content IS NOT NULL; | ||||
| UPDATE note_ancillary_contents SET content = 'text' WHERE content IS NOT NULL; | ||||
|  | ||||
| UPDATE attributes SET name = 'name', value = 'value' | ||||
|                   WHERE type = 'label' | ||||
|   | ||||
							
								
								
									
										20
									
								
								db/migrations/0213__note_ancillaries.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								db/migrations/0213__note_ancillaries.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| CREATE TABLE IF NOT EXISTS "note_ancillaries" | ||||
| ( | ||||
|     noteAncillaryId      TEXT not null primary key, | ||||
|     noteId       TEXT not null, | ||||
|     name         TEXT not null, | ||||
|     mime         TEXT not null, | ||||
|     isProtected    INT  not null DEFAULT 0, | ||||
|     contentCheckSum    TEXT not null, | ||||
|     utcDateModified TEXT not null, | ||||
|     isDeleted    INT  not null, | ||||
|     `deleteId`    TEXT DEFAULT NULL); | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS "note_ancillary_contents" (`noteAncillaryId`	TEXT NOT NULL PRIMARY KEY, | ||||
|                                                      `content`	TEXT DEFAULT NULL, | ||||
|                                                      `utcDateModified` TEXT NOT NULL); | ||||
|  | ||||
| CREATE INDEX IDX_note_ancillaries_name | ||||
|     on note_ancillaries (name); | ||||
| CREATE UNIQUE INDEX IDX_note_ancillaries_noteId_name | ||||
|     on note_ancillaries (noteId, name); | ||||
| @@ -112,3 +112,21 @@ CREATE TABLE IF NOT EXISTS "recent_notes" | ||||
|     notePath TEXT not null, | ||||
|     utcDateCreated TEXT not null | ||||
| ); | ||||
| CREATE TABLE IF NOT EXISTS "note_ancillaries" | ||||
| ( | ||||
|     noteAncillaryId      TEXT not null primary key, | ||||
|     noteId       TEXT not null, | ||||
|     name         TEXT not null, | ||||
|     mime         TEXT not null, | ||||
|     isProtected    INT  not null DEFAULT 0, | ||||
|     contentCheckSum    TEXT not null, | ||||
|     utcDateModified TEXT not null, | ||||
|     isDeleted    INT  not null, | ||||
|     `deleteId`    TEXT DEFAULT NULL); | ||||
| CREATE TABLE IF NOT EXISTS "note_ancillary_contents" (`noteAncillaryId`	TEXT NOT NULL PRIMARY KEY, | ||||
|                                                      `content`	TEXT DEFAULT NULL, | ||||
|                                                      `utcDateModified` TEXT NOT NULL); | ||||
| CREATE INDEX IDX_note_ancillaries_name | ||||
|     on note_ancillaries (name); | ||||
| CREATE UNIQUE INDEX IDX_note_ancillaries_noteId_name | ||||
|     on note_ancillaries (noteId, name); | ||||
|   | ||||
							
								
								
									
										678
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										678
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -34,7 +34,7 @@ | ||||
|     "@excalidraw/excalidraw": "0.14.2", | ||||
|     "archiver": "5.3.1", | ||||
|     "async-mutex": "0.4.0", | ||||
|     "axios": "1.3.3", | ||||
|     "axios": "1.3.4", | ||||
|     "better-sqlite3": "7.4.5", | ||||
|     "chokidar": "3.5.3", | ||||
|     "cls-hooked": "4.2.2", | ||||
| @@ -64,7 +64,7 @@ | ||||
|     "ini": "3.0.1", | ||||
|     "is-animated": "2.0.2", | ||||
|     "is-svg": "4.3.2", | ||||
|     "jimp": "0.22.4", | ||||
|     "jimp": "0.22.7", | ||||
|     "joplin-turndown-plugin-gfm": "1.0.12", | ||||
|     "jsdom": "21.1.0", | ||||
|     "mime-types": "2.1.35", | ||||
| @@ -101,7 +101,7 @@ | ||||
|     "electron-rebuild": "3.2.9", | ||||
|     "esm": "3.2.25", | ||||
|     "jasmine": "4.5.0", | ||||
|     "jsdoc": "4.0.1", | ||||
|     "jsdoc": "4.0.2", | ||||
|     "lorem-ipsum": "2.0.8", | ||||
|     "rcedit": "3.0.1", | ||||
|     "webpack": "5.75.0", | ||||
|   | ||||
| @@ -121,6 +121,14 @@ class Becca { | ||||
|         return row ? new BNoteRevision(row) : null; | ||||
|     } | ||||
|  | ||||
|     /** @returns {BNoteAncillary|null} */ | ||||
|     getNoteAncillary(noteAncillaryId) { | ||||
|         const row = sql.getRow("SELECT * FROM note_ancillaries WHERE noteAncillaryId = ?", [noteAncillaryId]); | ||||
|  | ||||
|         const BNoteAncillary = require("./entities/bnote_ancillary"); // avoiding circular dependency problems | ||||
|         return row ? new BNoteAncillary(row) : null; | ||||
|     } | ||||
|  | ||||
|     /** @returns {BOption|null} */ | ||||
|     getOption(name) { | ||||
|         return this.options[name]; | ||||
| @@ -143,6 +151,8 @@ class Becca { | ||||
|  | ||||
|         if (entityName === 'note_revisions') { | ||||
|             return this.getNoteRevision(entityId); | ||||
|         } else if (entityName === 'note_ancillaries') { | ||||
|             return this.getNoteAncillary(entityId); | ||||
|         } | ||||
|  | ||||
|         const camelCaseEntityName = entityName.toLowerCase().replace(/(_[a-z])/g, | ||||
|   | ||||
| @@ -198,6 +198,10 @@ class BBranch extends AbstractBeccaEntity { | ||||
|                 relation.markAsDeleted(deleteId); | ||||
|             } | ||||
|  | ||||
|             for (const noteAncillary of note.getNoteAncillaries()) { | ||||
|                 noteAncillary.markAsDeleted(deleteId); | ||||
|             } | ||||
|  | ||||
|             note.markAsDeleted(deleteId); | ||||
|  | ||||
|             return true; | ||||
|   | ||||
| @@ -8,6 +8,7 @@ const dateUtils = require('../../services/date_utils'); | ||||
| const entityChangesService = require('../../services/entity_changes'); | ||||
| const AbstractBeccaEntity = require("./abstract_becca_entity"); | ||||
| const BNoteRevision = require("./bnote_revision"); | ||||
| const BNoteAncillary = require("./bnote_ancillary"); | ||||
| const TaskContext = require("../../services/task_context"); | ||||
| const dayjs = require("dayjs"); | ||||
| const utc = require('dayjs/plugin/utc'); | ||||
| @@ -1135,6 +1136,19 @@ class BNote extends AbstractBeccaEntity { | ||||
|             .map(row => new BNoteRevision(row)); | ||||
|     } | ||||
|  | ||||
|     /** @returns {BNoteAncillary[]} */ | ||||
|     getNoteAncillaries() { | ||||
|         return sql.getRows("SELECT * FROM note_ancillaries WHERE noteId = ? AND isDeleted = 0", [this.noteId]) | ||||
|             .map(row => new BNoteAncillary(row)); | ||||
|     } | ||||
|  | ||||
|     /** @returns {BNoteAncillary|undefined} */ | ||||
|     getNoteAncillaryByName(name) { | ||||
|         return sql.getRows("SELECT * FROM note_ancillaries WHERE noteId = ? AND name = ? AND isDeleted = 0", [this.noteId, name]) | ||||
|             .map(row => new BNoteAncillary(row)) | ||||
|             [0]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @returns {string[][]} - array of notePaths (each represented by array of noteIds constituting the particular note path) | ||||
|      */ | ||||
| @@ -1462,6 +1476,31 @@ class BNote extends AbstractBeccaEntity { | ||||
|         return noteRevision; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @returns {BNoteAncillary} | ||||
|      */ | ||||
|     saveNoteAncillary(name, mime, content) { | ||||
|         let noteAncillary = this.getNoteAncillaryByName(name); | ||||
|  | ||||
|         if (noteAncillary | ||||
|             && noteAncillary.mime === mime | ||||
|             && noteAncillary.contentCheckSum === noteAncillary.calculateCheckSum(content)) { | ||||
|  | ||||
|             return noteAncillary; // no change | ||||
|         } | ||||
|  | ||||
|         noteAncillary = new BNoteAncillary({ | ||||
|             noteId: this.noteId, | ||||
|             name, | ||||
|             mime, | ||||
|             isProtected: this.isProtected | ||||
|         }); | ||||
|  | ||||
|         noteAncillary.setContent(content); | ||||
|  | ||||
|         return noteAncillary; | ||||
|     } | ||||
|  | ||||
|     beforeSaving() { | ||||
|         super.beforeSaving(); | ||||
|  | ||||
|   | ||||
							
								
								
									
										161
									
								
								src/becca/entities/bnote_ancillary.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								src/becca/entities/bnote_ancillary.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const protectedSessionService = require('../../services/protected_session'); | ||||
| const utils = require('../../services/utils'); | ||||
| const sql = require('../../services/sql'); | ||||
| const dateUtils = require('../../services/date_utils'); | ||||
| const becca = require('../becca'); | ||||
| const entityChangesService = require('../../services/entity_changes'); | ||||
| const AbstractBeccaEntity = require("./abstract_becca_entity"); | ||||
|  | ||||
| /** | ||||
|  * NoteAncillary represent data related/attached to the note. Conceptually similar to attributes, but intended for | ||||
|  * larger amounts of data and generally not accessible to the user. | ||||
|  * | ||||
|  * @extends AbstractBeccaEntity | ||||
|  */ | ||||
| class BNoteAncillary extends AbstractBeccaEntity { | ||||
|     static get entityName() { return "note_ancillaries"; } | ||||
|     static get primaryKeyName() { return "noteAncillaryId"; } | ||||
|     static get hashedProperties() { return ["noteAncillaryId", "noteId", "name", "content", "utcDateModified"]; } | ||||
|  | ||||
|     constructor(row) { | ||||
|         super(); | ||||
|  | ||||
|         if (!row.noteId) { | ||||
|             throw new Error("'noteId' must be given to initialize a NoteAncillary entity"); | ||||
|         } | ||||
|  | ||||
|         if (!row.name) { | ||||
|             throw new Error("'name' must be given to initialize a NoteAncillary entity"); | ||||
|         } | ||||
|  | ||||
|         /** @type {string} needs to be set at the initialization time since it's used in the .setContent() */ | ||||
|         this.noteAncillaryId = row.noteAncillaryId || `${this.noteId}_${this.name}`; | ||||
|         /** @type {string} */ | ||||
|         this.noteId = row.noteId; | ||||
|         /** @type {string} */ | ||||
|         this.name = row.name; | ||||
|         /** @type {string} */ | ||||
|         this.mime = row.mime; | ||||
|         /** @type {boolean} */ | ||||
|         this.isProtected = !!row.isProtected; | ||||
|         /** @type {string} */ | ||||
|         this.contentCheckSum = row.contentCheckSum; | ||||
|         /** @type {string} */ | ||||
|         this.utcDateModified = row.utcDateModified; | ||||
|     } | ||||
|  | ||||
|     getNote() { | ||||
|         return becca.notes[this.noteId]; | ||||
|     } | ||||
|  | ||||
|     /** @returns {boolean} true if the note has string content (not binary) */ | ||||
|     isStringNote() { | ||||
|         return utils.isStringNote(this.type, this.mime); | ||||
|     } | ||||
|  | ||||
|     /** @returns {*} */ | ||||
|     getContent(silentNotFoundError = false) { | ||||
|         const res = sql.getRow(`SELECT content FROM note_ancillary_contents WHERE noteAncillaryId = ?`, [this.noteAncillaryId]); | ||||
|  | ||||
|         if (!res) { | ||||
|             if (silentNotFoundError) { | ||||
|                 return undefined; | ||||
|             } | ||||
|             else { | ||||
|                 throw new Error(`Cannot find note ancillary content for noteAncillaryId=${this.noteAncillaryId}`); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         let content = res.content; | ||||
|  | ||||
|         if (this.isProtected) { | ||||
|             if (protectedSessionService.isProtectedSessionAvailable()) { | ||||
|                 content = protectedSessionService.decrypt(content); | ||||
|             } | ||||
|             else { | ||||
|                 content = ""; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (this.isStringNote()) { | ||||
|             return content === null | ||||
|                 ? "" | ||||
|                 : content.toString("UTF-8"); | ||||
|         } | ||||
|         else { | ||||
|             return content; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     setContent(content) { | ||||
|         sql.transactional(() => { | ||||
|             this.contentCheckSum = this.calculateCheckSum(content); | ||||
|             this.save(); // also explicitly save note_ancillary to update contentCheckSum | ||||
|  | ||||
|             const pojo = { | ||||
|                 noteAncillaryId: this.noteAncillaryId, | ||||
|                 content: content, | ||||
|                 utcDateModified: dateUtils.utcNowDateTime() | ||||
|             }; | ||||
|  | ||||
|             if (this.isProtected) { | ||||
|                 if (protectedSessionService.isProtectedSessionAvailable()) { | ||||
|                     pojo.content = protectedSessionService.encrypt(pojo.content); | ||||
|                 } else { | ||||
|                     throw new Error(`Cannot update content of noteAncillaryId=${this.noteAncillaryId} since we're out of protected session.`); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             sql.upsert("note_ancillary_contents", "noteAncillaryId", pojo); | ||||
|  | ||||
|             entityChangesService.addEntityChange({ | ||||
|                 entityName: 'note_ancillary_contents', | ||||
|                 entityId: this.noteAncillaryId, | ||||
|                 hash: this.contentCheckSum, | ||||
|                 isErased: false, | ||||
|                 utcDateChanged: pojo.utcDateModified, | ||||
|                 isSynced: true | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     calculateCheckSum(content) { | ||||
|         return utils.hash(`${this.noteAncillaryId}|${content.toString()}`); | ||||
|     } | ||||
|  | ||||
|     beforeSaving() { | ||||
|         if (!this.name.match(/^[a-z0-9]+$/i)) { | ||||
|             throw new Error(`Name must be alphanumerical, "${this.name}" given.`); | ||||
|         } | ||||
|  | ||||
|         this.noteAncillaryId = `${this.noteId}_${this.name}`; | ||||
|  | ||||
|         super.beforeSaving(); | ||||
|  | ||||
|         this.utcDateModified = dateUtils.utcNowDateTime(); | ||||
|     } | ||||
|  | ||||
|     getPojo() { | ||||
|         return { | ||||
|             noteAncillaryId: this.noteAncillaryId, | ||||
|             noteId: this.noteId, | ||||
|             name: this.name, | ||||
|             mime: this.mime, | ||||
|             isProtected: !!this.isProtected, | ||||
|             contentCheckSum: this.contentCheckSum, | ||||
|             isDeleted: false, | ||||
|             utcDateModified: this.utcDateModified | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     getPojoToSave() { | ||||
|         const pojo = this.getPojo(); | ||||
|         delete pojo.content; // not getting persisted | ||||
|  | ||||
|         return pojo; | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = BNoteAncillary; | ||||
| @@ -1,5 +1,6 @@ | ||||
| const BNote = require('./entities/bnote'); | ||||
| const BNoteRevision = require('./entities/bnote_revision'); | ||||
| const BNoteAncillary = require("./entities/bnote_ancillary"); | ||||
| const BBranch = require('./entities/bbranch'); | ||||
| const BAttribute = require('./entities/battribute'); | ||||
| const BRecentNote = require('./entities/brecent_note'); | ||||
| @@ -13,6 +14,8 @@ const ENTITY_NAME_TO_ENTITY = { | ||||
|     "note_contents": BNote, | ||||
|     "note_revisions": BNoteRevision, | ||||
|     "note_revision_contents": BNoteRevision, | ||||
|     "note_ancillaries": BNoteAncillary, | ||||
|     "note_ancillary_contents": BNoteAncillary, | ||||
|     "recent_notes": BRecentNote, | ||||
|     "etapi_tokens": BEtapiToken, | ||||
|     "options": BOption | ||||
|   | ||||
| @@ -36,7 +36,7 @@ async function processEntityChanges(entityChanges) { | ||||
|  | ||||
|                 loadResults.addOption(ec.entity.name); | ||||
|             } | ||||
|             else if (['etapi_tokens'].includes(ec.entityName)) { | ||||
|             else if (['etapi_tokens', 'note_ancillaries', 'note_ancillary_contents'].includes(ec.entityName)) { | ||||
|                 // NOOP | ||||
|             } | ||||
|             else { | ||||
|   | ||||
| @@ -28,6 +28,7 @@ const TPL = ` | ||||
|         <a data-trigger-command="renderActiveNote" class="dropdown-item render-note-button"><kbd data-command="renderActiveNote"></kbd> Re-render note</a> | ||||
|         <a data-trigger-command="findInText" class="dropdown-item find-in-text-button">Search in note <kbd data-command="findInText"></a> | ||||
|         <a data-trigger-command="showNoteSource" class="dropdown-item show-source-button"><kbd data-command="showNoteSource"></kbd> Note source</a> | ||||
|         <a data-trigger-command="showNoteAncillaries" class="dropdown-item"><kbd data-command="showNoteAncillaries"></kbd> Note ancillaries</a> | ||||
|         <a data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button"><kbd data-command="openNoteExternally"></kbd> Open note externally</a> | ||||
|         <a class="dropdown-item import-files-button">Import files</a> | ||||
|         <a class="dropdown-item export-note-button">Export note</a> | ||||
|   | ||||
| @@ -27,6 +27,7 @@ import NoteMapTypeWidget from "./type_widgets/note_map.js"; | ||||
| import WebViewTypeWidget from "./type_widgets/web_view.js"; | ||||
| import DocTypeWidget from "./type_widgets/doc.js"; | ||||
| import ContentWidgetTypeWidget from "./type_widgets/content_widget.js"; | ||||
| import AncillariesTypeWidget from "./type_widgets/ancillaries.js"; | ||||
|  | ||||
| const TPL = ` | ||||
| <div class="note-detail"> | ||||
| @@ -61,7 +62,8 @@ const typeWidgetClasses = { | ||||
|     'noteMap': NoteMapTypeWidget, | ||||
|     'webView': WebViewTypeWidget, | ||||
|     'doc': DocTypeWidget, | ||||
|     'contentWidget': ContentWidgetTypeWidget | ||||
|     'contentWidget': ContentWidgetTypeWidget, | ||||
|     'ancillaries': AncillariesTypeWidget | ||||
| }; | ||||
|  | ||||
| export default class NoteDetailWidget extends NoteContextAwareWidget { | ||||
| @@ -189,6 +191,8 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { | ||||
|  | ||||
|         if (type === 'text' && this.noteContext.viewScope.viewMode === 'source') { | ||||
|             type = 'readOnlyCode'; | ||||
|         } else if (this.noteContext.viewScope.viewMode === 'ancillaries') { | ||||
|             type = 'ancillaries'; | ||||
|         } else if (type === 'text' && await this.noteContext.isReadOnly()) { | ||||
|             type = 'readOnlyText'; | ||||
|         } else if ((type === 'code' || type === 'mermaid') && await this.noteContext.isReadOnly()) { | ||||
|   | ||||
							
								
								
									
										79
									
								
								src/public/app/widgets/type_widgets/ancillaries.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/public/app/widgets/type_widgets/ancillaries.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| import TypeWidget from "./type_widget.js"; | ||||
| import server from "../../services/server.js"; | ||||
|  | ||||
| const TPL = ` | ||||
| <div class="note-ancillaries note-detail-printable"> | ||||
|     <style> | ||||
|         .note-ancillaries { | ||||
|             padding: 15px; | ||||
|         } | ||||
|          | ||||
|         .ancillary-content { | ||||
|             max-height: 400px; | ||||
|             background: var(--accented-background-color); | ||||
|             padding: 10px; | ||||
|             margin-top: 10px; | ||||
|             margin-bottom: 10px; | ||||
|         } | ||||
|          | ||||
|         .ancillary-details th { | ||||
|             padding-left: 10px; | ||||
|             padding-right: 10px; | ||||
|         } | ||||
|     </style> | ||||
|  | ||||
|     <div class="alert alert-info" style="margin: 10px 0 10px 0; padding: 20px;"> | ||||
|         Note ancillaries are pieces of data attached to a given note, providing ancillary support.  | ||||
|         This view is useful for diagnostics. | ||||
|     </div> | ||||
|      | ||||
|     <div class="note-ancillary-list"></div> | ||||
| </div>`; | ||||
|  | ||||
| export default class AncillariesTypeWidget extends TypeWidget { | ||||
|     static getType() { return "ancillaries"; } | ||||
|  | ||||
|     doRender() { | ||||
|         this.$widget = $(TPL); | ||||
|         this.$list = this.$widget.find('.note-ancillary-list'); | ||||
|  | ||||
|         super.doRender(); | ||||
|     } | ||||
|  | ||||
|     async doRefresh(note) { | ||||
|         this.$list.empty(); | ||||
|  | ||||
|         const ancillaries = await server.get(`notes/${this.noteId}/ancillaries?includeContent=true`); | ||||
|  | ||||
|         if (ancillaries.length === 0) { | ||||
|             this.$list.html("<strong>This note has no ancillaries.</strong>"); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         for (const ancillary of ancillaries) { | ||||
|             this.$list.append( | ||||
|                 $('<div class="note-ancillary-wrapper">') | ||||
|                     .append( | ||||
|                         $('<h4>').append($('<span class="ancillary-name">').text(ancillary.name)) | ||||
|                     ) | ||||
|                     .append( | ||||
|                         $('<table class="ancillary-details">') | ||||
|                             .append( | ||||
|                                 $('<tr>') | ||||
|                                     .append($('<th>').text('Length:')) | ||||
|                                     .append($('<td>').text(ancillary.contentLength)) | ||||
|                                     .append($('<th>').text('MIME:')) | ||||
|                                     .append($('<td>').text(ancillary.mime)) | ||||
|                                     .append($('<th>').text('Date modified:')) | ||||
|                                     .append($('<td>').text(ancillary.utcDateModified)) | ||||
|                             ) | ||||
|                     ) | ||||
|                     .append( | ||||
|                         $('<pre class="ancillary-content">') | ||||
|                             .text(ancillary.content) | ||||
|                     ) | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -54,10 +54,10 @@ function createNote(req) { | ||||
| } | ||||
|  | ||||
| function updateNoteData(req) { | ||||
|     const {content} = req.body; | ||||
|     const {content, ancillaries} = req.body; | ||||
|     const {noteId} = req.params; | ||||
|  | ||||
|     return noteService.updateNoteData(noteId, content); | ||||
|     return noteService.updateNoteData(noteId, content, ancillaries); | ||||
| } | ||||
|  | ||||
| function deleteNote(req) { | ||||
| @@ -127,6 +127,49 @@ function setNoteTypeMime(req) { | ||||
|     note.save(); | ||||
| } | ||||
|  | ||||
| function getNoteAncillaries(req) { | ||||
|     const includeContent = req.query.includeContent === 'true'; | ||||
|     const {noteId} = req.params; | ||||
|  | ||||
|     const note = becca.getNote(noteId); | ||||
|  | ||||
|     if (!note) { | ||||
|         throw new NotFoundError(`Note '${noteId}' doesn't exist.`); | ||||
|     } | ||||
|  | ||||
|     const noteAncillaries = note.getNoteAncillaries(); | ||||
|  | ||||
|     return noteAncillaries.map(ancillary => { | ||||
|        const pojo = ancillary.getPojo(); | ||||
|  | ||||
|        if (includeContent && utils.isStringNote(null, ancillary.mime)) { | ||||
|            pojo.content = ancillary.getContent()?.toString(); | ||||
|            pojo.contentLength = pojo.content.length; | ||||
|  | ||||
|            const MAX_ANCILLARY_LENGTH = 1_000_000; | ||||
|  | ||||
|            if (pojo.content.length > MAX_ANCILLARY_LENGTH) { | ||||
|                pojo.content = pojo.content.substring(0, MAX_ANCILLARY_LENGTH); | ||||
|            } | ||||
|        } | ||||
|  | ||||
|        return pojo; | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function saveNoteAncillary(req) { | ||||
|     const {noteId, name} = req.params; | ||||
|     const {mime, content} = req.body; | ||||
|  | ||||
|     const note = becca.getNote(noteId); | ||||
|  | ||||
|     if (!note) { | ||||
|         throw new NotFoundError(`Note '${noteId}' doesn't exist.`); | ||||
|     } | ||||
|  | ||||
|     note.saveNoteAncillary(name, mime, content); | ||||
| } | ||||
|  | ||||
| function getRelationMap(req) { | ||||
|     const {relationMapNoteId, noteIds} = req.body; | ||||
|  | ||||
| @@ -340,5 +383,7 @@ module.exports = { | ||||
|     eraseDeletedNotesNow, | ||||
|     getDeleteNotesPreview, | ||||
|     uploadModifiedFile, | ||||
|     forceSaveNoteRevision | ||||
|     forceSaveNoteRevision, | ||||
|     getNoteAncillaries, | ||||
|     saveNoteAncillary | ||||
| }; | ||||
|   | ||||
| @@ -114,6 +114,14 @@ function forceNoteSync(req) { | ||||
|         entityChangesService.moveEntityChangeToTop('note_revision_contents', noteRevisionId); | ||||
|     } | ||||
|  | ||||
|     for (const noteAncillaryId of sql.getColumn("SELECT noteAncillaryId FROM note_ancillaries WHERE noteId = ?", [noteId])) { | ||||
|         sql.execute(`UPDATE note_ancillaries SET utcDateModified = ? WHERE noteAncillaryId = ?`, [now, noteAncillaryId]); | ||||
|         entityChangesService.moveEntityChangeToTop('note_ancillaries', noteAncillaryId); | ||||
|  | ||||
|         sql.execute(`UPDATE note_ancillary_contents SET utcDateModified = ? WHERE noteAncillaryId = ?`, [now, noteAncillaryId]); | ||||
|         entityChangesService.moveEntityChangeToTop('note_ancillary_contents', noteAncillaryId); | ||||
|     } | ||||
|  | ||||
|     log.info(`Forcing note sync for ${noteId}`); | ||||
|  | ||||
|     // not awaiting for the job to finish (will probably take a long time) | ||||
|   | ||||
| @@ -126,6 +126,8 @@ function register(app) { | ||||
|     apiRoute(PUT, '/api/notes/:noteId/sort-children', notesApiRoute.sortChildNotes); | ||||
|     apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectNote); | ||||
|     apiRoute(PUT, '/api/notes/:noteId/type', notesApiRoute.setNoteTypeMime); | ||||
|     apiRoute(GET, '/api/notes/:noteId/ancillaries', notesApiRoute.getNoteAncillaries); | ||||
|     apiRoute(PUT, '/api/notes/:noteId/ancillaries/:name', notesApiRoute.saveNoteAncillary); | ||||
|     apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions); | ||||
|     apiRoute(DELETE, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.eraseAllNoteRevisions); | ||||
|     apiRoute(GET, '/api/notes/:noteId/revisions/:noteRevisionId', noteRevisionsApiRoute.getNoteRevision); | ||||
|   | ||||
| @@ -4,8 +4,8 @@ const build = require('./build'); | ||||
| const packageJson = require('../../package'); | ||||
| const {TRILIUM_DATA_DIR} = require('./data_dir'); | ||||
|  | ||||
| const APP_DB_VERSION = 213; | ||||
| const SYNC_VERSION = 29; | ||||
| const APP_DB_VERSION = 214; | ||||
| const SYNC_VERSION = 30; | ||||
| const CLIPPER_PROTOCOL_VERSION = "1.0"; | ||||
|  | ||||
| module.exports = { | ||||
|   | ||||
| @@ -213,6 +213,25 @@ class ConsistencyChecks { | ||||
|                     logError(`Relation '${attributeId}' references missing note '${noteId}'`) | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|         this.findAndFixIssues(` | ||||
|                     SELECT noteAncillaryId, note_ancillaries.noteId AS noteId | ||||
|                     FROM note_ancillaries | ||||
|                       LEFT JOIN notes USING (noteId) | ||||
|                     WHERE notes.noteId IS NULL | ||||
|                       AND note_ancillaries.isDeleted = 0`, | ||||
|             ({noteAncillaryId, noteId}) => { | ||||
|                 if (this.autoFix) { | ||||
|                     const noteAncillary = becca.getNoteAncillary(noteAncillaryId); | ||||
|                     noteAncillary.markAsDeleted(); | ||||
|  | ||||
|                     this.reloadNeeded = false; | ||||
|  | ||||
|                     logFix(`Note ancillary '${noteAncillaryId}' has been deleted since it references missing note '${noteId}'`); | ||||
|                 } else { | ||||
|                     logError(`Note ancillary '${noteAncillaryId}' references missing note '${noteId}'`); | ||||
|                 } | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     findExistencyIssues() { | ||||
| @@ -320,6 +339,26 @@ class ConsistencyChecks { | ||||
|                     logError(`Duplicate branches for note '${noteId}' and parent '${parentNoteId}'`); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|         this.findAndFixIssues(` | ||||
|                     SELECT noteAncillaryId, | ||||
|                            note_ancillaries.noteId AS noteId | ||||
|                     FROM note_ancillaries | ||||
|                       JOIN notes USING (noteId) | ||||
|                     WHERE notes.isDeleted = 1 | ||||
|                       AND note_ancillaries.isDeleted = 0`, | ||||
|             ({noteAncillaryId, noteId}) => { | ||||
|                 if (this.autoFix) { | ||||
|                     const noteAncillary = becca.getNoteAncillary(noteAncillaryId); | ||||
|                     noteAncillary.markAsDeleted(); | ||||
|  | ||||
|                     this.reloadNeeded = false; | ||||
|  | ||||
|                     logFix(`Note ancillary '${noteAncillaryId}' has been deleted since associated note '${noteId}' is deleted.`); | ||||
|                 } else { | ||||
|                     logError(`Note ancillary '${noteAncillaryId}' is not deleted even though associated note '${noteId}' is deleted.`) | ||||
|                 } | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     findLogicIssues() { | ||||
| @@ -620,6 +659,8 @@ class ConsistencyChecks { | ||||
|         this.runEntityChangeChecks("note_contents", "noteId"); | ||||
|         this.runEntityChangeChecks("note_revisions", "noteRevisionId"); | ||||
|         this.runEntityChangeChecks("note_revision_contents", "noteRevisionId"); | ||||
|         this.runEntityChangeChecks("note_ancillaries", "noteAncillaryId"); | ||||
|         this.runEntityChangeChecks("note_ancillary_contents", "noteAncillaryId"); | ||||
|         this.runEntityChangeChecks("branches", "branchId"); | ||||
|         this.runEntityChangeChecks("attributes", "attributeId"); | ||||
|         this.runEntityChangeChecks("etapi_tokens", "etapiTokenId"); | ||||
| @@ -715,7 +756,7 @@ class ConsistencyChecks { | ||||
|             return `${tableName}: ${count}`; | ||||
|         } | ||||
|  | ||||
|         const tables = [ "notes", "note_revisions", "branches", "attributes", "etapi_tokens" ]; | ||||
|         const tables = [ "notes", "note_revisions", "note_ancillaries", "branches", "attributes", "etapi_tokens" ]; | ||||
|  | ||||
|         log.info(`Table counts: ${tables.map(tableName => getTableRowCount(tableName)).join(", ")}`); | ||||
|     } | ||||
|   | ||||
| @@ -151,6 +151,8 @@ function fillAllEntityChanges() { | ||||
|         fillEntityChanges("branches", "branchId"); | ||||
|         fillEntityChanges("note_revisions", "noteRevisionId"); | ||||
|         fillEntityChanges("note_revision_contents", "noteRevisionId"); | ||||
|         fillEntityChanges("note_ancillaries", "noteAncillaryId"); | ||||
|         fillEntityChanges("note_ancillary_contents", "noteAncillaryId"); | ||||
|         fillEntityChanges("attributes", "attributeId"); | ||||
|         fillEntityChanges("etapi_tokens", "etapiTokenId"); | ||||
|         fillEntityChanges("options", "name", 'isSynced = 1'); | ||||
|   | ||||
| @@ -170,6 +170,24 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) | ||||
|             meta.dataFileName = getDataFileName(note.type, note.mime, baseFileName, existingFileNames); | ||||
|         } | ||||
|  | ||||
|         const ancillaries = note.getNoteAncillaries(); | ||||
|  | ||||
|         if (ancillaries.length > 0) { | ||||
|             meta.ancillaries = ancillaries | ||||
|                 .filter(ancillary => ["canvasSvg", "mermaidSvg"].includes(ancillary.name)) | ||||
|                 .map(ancillary => ({ | ||||
|  | ||||
|                 name: ancillary.name, | ||||
|                 mime: ancillary.mime, | ||||
|                 dataFileName: getDataFileName( | ||||
|                     null, | ||||
|                     ancillary.mime, | ||||
|                     baseFileName + "_" + ancillary.name, | ||||
|                     existingFileNames | ||||
|                 ) | ||||
|             })); | ||||
|         } | ||||
|  | ||||
|         if (childBranches.length > 0) { | ||||
|             meta.dirFileName = getUniqueFilename(existingFileNames, baseFileName); | ||||
|             meta.children = []; | ||||
| @@ -319,6 +337,16 @@ ${markdownContent}`; | ||||
|  | ||||
|         taskContext.increaseProgressCount(); | ||||
|  | ||||
|         for (const ancillaryMeta of noteMeta.ancillaries || []) { | ||||
|             const noteAncillary = note.getNoteAncillaryByName(ancillaryMeta.name); | ||||
|             const content = noteAncillary.getContent(); | ||||
|  | ||||
|             archive.append(content, { | ||||
|                 name: filePathPrefix + ancillaryMeta.dataFileName, | ||||
|                 date: dateUtils.parseDateTime(note.utcDateModified) | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         if (noteMeta.children && noteMeta.children.length > 0) { | ||||
|             const directoryPath = filePathPrefix + noteMeta.dirFileName; | ||||
|  | ||||
|   | ||||
| @@ -14,6 +14,7 @@ const treeService = require("../tree"); | ||||
| const yauzl = require("yauzl"); | ||||
| const htmlSanitizer = require('../html_sanitizer'); | ||||
| const becca = require("../../becca/becca"); | ||||
| const BNoteAncillary = require("../../becca/entities/bnote_ancillary"); | ||||
|  | ||||
| /** | ||||
|  * @param {TaskContext} taskContext | ||||
| @@ -64,6 +65,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) { | ||||
|         }; | ||||
|  | ||||
|         let parent; | ||||
|         let ancillaryMeta = false; | ||||
|  | ||||
|         for (const segment of pathSegments) { | ||||
|             if (!cursor || !cursor.children || cursor.children.length === 0) { | ||||
| @@ -72,11 +74,28 @@ async function importZip(taskContext, fileBuffer, importRootNote) { | ||||
|  | ||||
|             parent = cursor; | ||||
|             cursor = parent.children.find(file => file.dataFileName === segment || file.dirFileName === segment); | ||||
|  | ||||
|             if (!cursor) { | ||||
|                 for (const file of parent.children) { | ||||
|                     for (const ancillary of file.ancillaries || []) { | ||||
|                         if (ancillary.dataFileName === segment) { | ||||
|                             cursor = file; | ||||
|                             ancillaryMeta = ancillary; | ||||
|                             break; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     if (cursor) { | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             parentNoteMeta: parent, | ||||
|             noteMeta: cursor | ||||
|             noteMeta: cursor, | ||||
|             ancillaryMeta | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| @@ -351,7 +370,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) { | ||||
|     } | ||||
|  | ||||
|     function saveNote(filePath, content) { | ||||
|         const {parentNoteMeta, noteMeta} = getMeta(filePath); | ||||
|         const {parentNoteMeta, noteMeta, ancillaryMeta} = getMeta(filePath); | ||||
|  | ||||
|         if (noteMeta?.noImport) { | ||||
|             return; | ||||
| @@ -359,6 +378,17 @@ async function importZip(taskContext, fileBuffer, importRootNote) { | ||||
|  | ||||
|         const noteId = getNoteId(noteMeta, filePath); | ||||
|  | ||||
|         if (ancillaryMeta) { | ||||
|             const noteAncillary = new BNoteAncillary({ | ||||
|                 noteId, | ||||
|                 name: ancillaryMeta.name, | ||||
|                 mime: ancillaryMeta.mime | ||||
|             }); | ||||
|  | ||||
|             noteAncillary.setContent(content); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const parentNoteId = getParentNoteId(filePath, parentNoteMeta); | ||||
|  | ||||
|         if (!parentNoteId) { | ||||
|   | ||||
							
								
								
									
										37
									
								
								src/services/note_ancillaries.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/services/note_ancillaries.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| const protectedSession = require("./protected_session"); | ||||
| const log = require("./log"); | ||||
|  | ||||
| /** | ||||
|  * @param {BNote} note | ||||
|  */ | ||||
| function protectNoteAncillaries(note) { | ||||
|     for (const noteAncillary of note.getNoteAncillaries()) { | ||||
|         if (note.isProtected !== noteAncillary.isProtected) { | ||||
|             if (!protectedSession.isProtectedSessionAvailable()) { | ||||
|                 log.error("Protected session is not available to fix note ancillaries."); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             try { | ||||
|                 const content = noteAncillary.getContent(); | ||||
|  | ||||
|                 noteAncillary.isProtected = note.isProtected; | ||||
|  | ||||
|                 // this will force de/encryption | ||||
|                 noteAncillary.setContent(content); | ||||
|  | ||||
|                 noteAncillary.save(); | ||||
|             } | ||||
|             catch (e) { | ||||
|                 log.error(`Could not un/protect note ancillary ID = ${noteAncillary.noteAncillaryId}`); | ||||
|  | ||||
|                 throw e; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     protectNoteAncillaries | ||||
| } | ||||
| @@ -321,7 +321,7 @@ function getEntityChangeRow(entityName, entityId) { | ||||
|             throw new Error(`Entity ${entityName} ${entityId} not found.`); | ||||
|         } | ||||
|  | ||||
|         if (['note_contents', 'note_revision_contents'].includes(entityName) && entity.content !== null) { | ||||
|         if (['note_contents', 'note_revision_contents', 'note_ancillary_contents'].includes(entityName) && entity.content !== null) { | ||||
|             if (typeof entity.content === 'string') { | ||||
|                 entity.content = Buffer.from(entity.content, 'UTF-8'); | ||||
|             } | ||||
|   | ||||
| @@ -64,7 +64,7 @@ function updateNormalEntity(remoteEntityChange, remoteEntityRow, instanceId) { | ||||
|         || localEntityChange.utcDateChanged < remoteEntityChange.utcDateChanged | ||||
|         || localEntityChange.hash !== remoteEntityChange.hash // sync error, we should still update | ||||
|     ) { | ||||
|         if (['note_contents', 'note_revision_contents'].includes(remoteEntityChange.entityName)) { | ||||
|         if (['note_contents', 'note_revision_contents', 'note_ancillary_contents'].includes(remoteEntityChange.entityName)) { | ||||
|             remoteEntityRow.content = handleContent(remoteEntityRow.content); | ||||
|         } | ||||
|  | ||||
| @@ -115,7 +115,9 @@ function eraseEntity(entityChange, instanceId) { | ||||
|         "branches", | ||||
|         "attributes", | ||||
|         "note_revisions", | ||||
|         "note_revision_contents" | ||||
|         "note_revision_contents", | ||||
|         "note_ancillaries", | ||||
|         "note_ancillary_contents" | ||||
|     ]; | ||||
|  | ||||
|     if (!entityNames.includes(entityName)) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user