mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +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_contents SET content = 'text' WHERE content IS NOT NULL; | ||||||
| UPDATE note_revisions SET title = 'title'; | UPDATE note_revisions SET title = 'title'; | ||||||
| UPDATE note_revision_contents SET content = 'text' WHERE content IS NOT NULL; | 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' | UPDATE attributes SET name = 'name', value = 'value' | ||||||
|                   WHERE type = 'label' |                   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, |     notePath TEXT not null, | ||||||
|     utcDateCreated 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", |     "@excalidraw/excalidraw": "0.14.2", | ||||||
|     "archiver": "5.3.1", |     "archiver": "5.3.1", | ||||||
|     "async-mutex": "0.4.0", |     "async-mutex": "0.4.0", | ||||||
|     "axios": "1.3.3", |     "axios": "1.3.4", | ||||||
|     "better-sqlite3": "7.4.5", |     "better-sqlite3": "7.4.5", | ||||||
|     "chokidar": "3.5.3", |     "chokidar": "3.5.3", | ||||||
|     "cls-hooked": "4.2.2", |     "cls-hooked": "4.2.2", | ||||||
| @@ -64,7 +64,7 @@ | |||||||
|     "ini": "3.0.1", |     "ini": "3.0.1", | ||||||
|     "is-animated": "2.0.2", |     "is-animated": "2.0.2", | ||||||
|     "is-svg": "4.3.2", |     "is-svg": "4.3.2", | ||||||
|     "jimp": "0.22.4", |     "jimp": "0.22.7", | ||||||
|     "joplin-turndown-plugin-gfm": "1.0.12", |     "joplin-turndown-plugin-gfm": "1.0.12", | ||||||
|     "jsdom": "21.1.0", |     "jsdom": "21.1.0", | ||||||
|     "mime-types": "2.1.35", |     "mime-types": "2.1.35", | ||||||
| @@ -101,7 +101,7 @@ | |||||||
|     "electron-rebuild": "3.2.9", |     "electron-rebuild": "3.2.9", | ||||||
|     "esm": "3.2.25", |     "esm": "3.2.25", | ||||||
|     "jasmine": "4.5.0", |     "jasmine": "4.5.0", | ||||||
|     "jsdoc": "4.0.1", |     "jsdoc": "4.0.2", | ||||||
|     "lorem-ipsum": "2.0.8", |     "lorem-ipsum": "2.0.8", | ||||||
|     "rcedit": "3.0.1", |     "rcedit": "3.0.1", | ||||||
|     "webpack": "5.75.0", |     "webpack": "5.75.0", | ||||||
|   | |||||||
| @@ -121,6 +121,14 @@ class Becca { | |||||||
|         return row ? new BNoteRevision(row) : null; |         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} */ |     /** @returns {BOption|null} */ | ||||||
|     getOption(name) { |     getOption(name) { | ||||||
|         return this.options[name]; |         return this.options[name]; | ||||||
| @@ -143,6 +151,8 @@ class Becca { | |||||||
|  |  | ||||||
|         if (entityName === 'note_revisions') { |         if (entityName === 'note_revisions') { | ||||||
|             return this.getNoteRevision(entityId); |             return this.getNoteRevision(entityId); | ||||||
|  |         } else if (entityName === 'note_ancillaries') { | ||||||
|  |             return this.getNoteAncillary(entityId); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const camelCaseEntityName = entityName.toLowerCase().replace(/(_[a-z])/g, |         const camelCaseEntityName = entityName.toLowerCase().replace(/(_[a-z])/g, | ||||||
|   | |||||||
| @@ -198,6 +198,10 @@ class BBranch extends AbstractBeccaEntity { | |||||||
|                 relation.markAsDeleted(deleteId); |                 relation.markAsDeleted(deleteId); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             for (const noteAncillary of note.getNoteAncillaries()) { | ||||||
|  |                 noteAncillary.markAsDeleted(deleteId); | ||||||
|  |             } | ||||||
|  |  | ||||||
|             note.markAsDeleted(deleteId); |             note.markAsDeleted(deleteId); | ||||||
|  |  | ||||||
|             return true; |             return true; | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ const dateUtils = require('../../services/date_utils'); | |||||||
| const entityChangesService = require('../../services/entity_changes'); | const entityChangesService = require('../../services/entity_changes'); | ||||||
| const AbstractBeccaEntity = require("./abstract_becca_entity"); | const AbstractBeccaEntity = require("./abstract_becca_entity"); | ||||||
| const BNoteRevision = require("./bnote_revision"); | const BNoteRevision = require("./bnote_revision"); | ||||||
|  | const BNoteAncillary = require("./bnote_ancillary"); | ||||||
| const TaskContext = require("../../services/task_context"); | const TaskContext = require("../../services/task_context"); | ||||||
| const dayjs = require("dayjs"); | const dayjs = require("dayjs"); | ||||||
| const utc = require('dayjs/plugin/utc'); | const utc = require('dayjs/plugin/utc'); | ||||||
| @@ -1135,6 +1136,19 @@ class BNote extends AbstractBeccaEntity { | |||||||
|             .map(row => new BNoteRevision(row)); |             .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) |      * @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; |         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() { |     beforeSaving() { | ||||||
|         super.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 BNote = require('./entities/bnote'); | ||||||
| const BNoteRevision = require('./entities/bnote_revision'); | const BNoteRevision = require('./entities/bnote_revision'); | ||||||
|  | const BNoteAncillary = require("./entities/bnote_ancillary"); | ||||||
| const BBranch = require('./entities/bbranch'); | const BBranch = require('./entities/bbranch'); | ||||||
| const BAttribute = require('./entities/battribute'); | const BAttribute = require('./entities/battribute'); | ||||||
| const BRecentNote = require('./entities/brecent_note'); | const BRecentNote = require('./entities/brecent_note'); | ||||||
| @@ -13,6 +14,8 @@ const ENTITY_NAME_TO_ENTITY = { | |||||||
|     "note_contents": BNote, |     "note_contents": BNote, | ||||||
|     "note_revisions": BNoteRevision, |     "note_revisions": BNoteRevision, | ||||||
|     "note_revision_contents": BNoteRevision, |     "note_revision_contents": BNoteRevision, | ||||||
|  |     "note_ancillaries": BNoteAncillary, | ||||||
|  |     "note_ancillary_contents": BNoteAncillary, | ||||||
|     "recent_notes": BRecentNote, |     "recent_notes": BRecentNote, | ||||||
|     "etapi_tokens": BEtapiToken, |     "etapi_tokens": BEtapiToken, | ||||||
|     "options": BOption |     "options": BOption | ||||||
|   | |||||||
| @@ -36,7 +36,7 @@ async function processEntityChanges(entityChanges) { | |||||||
|  |  | ||||||
|                 loadResults.addOption(ec.entity.name); |                 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 |                 // NOOP | ||||||
|             } |             } | ||||||
|             else { |             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="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="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="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 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 import-files-button">Import files</a> | ||||||
|         <a class="dropdown-item export-note-button">Export note</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 WebViewTypeWidget from "./type_widgets/web_view.js"; | ||||||
| import DocTypeWidget from "./type_widgets/doc.js"; | import DocTypeWidget from "./type_widgets/doc.js"; | ||||||
| import ContentWidgetTypeWidget from "./type_widgets/content_widget.js"; | import ContentWidgetTypeWidget from "./type_widgets/content_widget.js"; | ||||||
|  | import AncillariesTypeWidget from "./type_widgets/ancillaries.js"; | ||||||
|  |  | ||||||
| const TPL = ` | const TPL = ` | ||||||
| <div class="note-detail"> | <div class="note-detail"> | ||||||
| @@ -61,7 +62,8 @@ const typeWidgetClasses = { | |||||||
|     'noteMap': NoteMapTypeWidget, |     'noteMap': NoteMapTypeWidget, | ||||||
|     'webView': WebViewTypeWidget, |     'webView': WebViewTypeWidget, | ||||||
|     'doc': DocTypeWidget, |     'doc': DocTypeWidget, | ||||||
|     'contentWidget': ContentWidgetTypeWidget |     'contentWidget': ContentWidgetTypeWidget, | ||||||
|  |     'ancillaries': AncillariesTypeWidget | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default class NoteDetailWidget extends NoteContextAwareWidget { | export default class NoteDetailWidget extends NoteContextAwareWidget { | ||||||
| @@ -189,6 +191,8 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { | |||||||
|  |  | ||||||
|         if (type === 'text' && this.noteContext.viewScope.viewMode === 'source') { |         if (type === 'text' && this.noteContext.viewScope.viewMode === 'source') { | ||||||
|             type = 'readOnlyCode'; |             type = 'readOnlyCode'; | ||||||
|  |         } else if (this.noteContext.viewScope.viewMode === 'ancillaries') { | ||||||
|  |             type = 'ancillaries'; | ||||||
|         } else if (type === 'text' && await this.noteContext.isReadOnly()) { |         } else if (type === 'text' && await this.noteContext.isReadOnly()) { | ||||||
|             type = 'readOnlyText'; |             type = 'readOnlyText'; | ||||||
|         } else if ((type === 'code' || type === 'mermaid') && await this.noteContext.isReadOnly()) { |         } 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) { | function updateNoteData(req) { | ||||||
|     const {content} = req.body; |     const {content, ancillaries} = req.body; | ||||||
|     const {noteId} = req.params; |     const {noteId} = req.params; | ||||||
|  |  | ||||||
|     return noteService.updateNoteData(noteId, content); |     return noteService.updateNoteData(noteId, content, ancillaries); | ||||||
| } | } | ||||||
|  |  | ||||||
| function deleteNote(req) { | function deleteNote(req) { | ||||||
| @@ -127,6 +127,49 @@ function setNoteTypeMime(req) { | |||||||
|     note.save(); |     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) { | function getRelationMap(req) { | ||||||
|     const {relationMapNoteId, noteIds} = req.body; |     const {relationMapNoteId, noteIds} = req.body; | ||||||
|  |  | ||||||
| @@ -340,5 +383,7 @@ module.exports = { | |||||||
|     eraseDeletedNotesNow, |     eraseDeletedNotesNow, | ||||||
|     getDeleteNotesPreview, |     getDeleteNotesPreview, | ||||||
|     uploadModifiedFile, |     uploadModifiedFile, | ||||||
|     forceSaveNoteRevision |     forceSaveNoteRevision, | ||||||
|  |     getNoteAncillaries, | ||||||
|  |     saveNoteAncillary | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -114,6 +114,14 @@ function forceNoteSync(req) { | |||||||
|         entityChangesService.moveEntityChangeToTop('note_revision_contents', noteRevisionId); |         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}`); |     log.info(`Forcing note sync for ${noteId}`); | ||||||
|  |  | ||||||
|     // not awaiting for the job to finish (will probably take a long time) |     // 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/sort-children', notesApiRoute.sortChildNotes); | ||||||
|     apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectNote); |     apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectNote); | ||||||
|     apiRoute(PUT, '/api/notes/:noteId/type', notesApiRoute.setNoteTypeMime); |     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(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions); | ||||||
|     apiRoute(DELETE, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.eraseAllNoteRevisions); |     apiRoute(DELETE, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.eraseAllNoteRevisions); | ||||||
|     apiRoute(GET, '/api/notes/:noteId/revisions/:noteRevisionId', noteRevisionsApiRoute.getNoteRevision); |     apiRoute(GET, '/api/notes/:noteId/revisions/:noteRevisionId', noteRevisionsApiRoute.getNoteRevision); | ||||||
|   | |||||||
| @@ -4,8 +4,8 @@ const build = require('./build'); | |||||||
| const packageJson = require('../../package'); | const packageJson = require('../../package'); | ||||||
| const {TRILIUM_DATA_DIR} = require('./data_dir'); | const {TRILIUM_DATA_DIR} = require('./data_dir'); | ||||||
|  |  | ||||||
| const APP_DB_VERSION = 213; | const APP_DB_VERSION = 214; | ||||||
| const SYNC_VERSION = 29; | const SYNC_VERSION = 30; | ||||||
| const CLIPPER_PROTOCOL_VERSION = "1.0"; | const CLIPPER_PROTOCOL_VERSION = "1.0"; | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|   | |||||||
| @@ -213,6 +213,25 @@ class ConsistencyChecks { | |||||||
|                     logError(`Relation '${attributeId}' references missing note '${noteId}'`) |                     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() { |     findExistencyIssues() { | ||||||
| @@ -320,6 +339,26 @@ class ConsistencyChecks { | |||||||
|                     logError(`Duplicate branches for note '${noteId}' and parent '${parentNoteId}'`); |                     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() { |     findLogicIssues() { | ||||||
| @@ -620,6 +659,8 @@ class ConsistencyChecks { | |||||||
|         this.runEntityChangeChecks("note_contents", "noteId"); |         this.runEntityChangeChecks("note_contents", "noteId"); | ||||||
|         this.runEntityChangeChecks("note_revisions", "noteRevisionId"); |         this.runEntityChangeChecks("note_revisions", "noteRevisionId"); | ||||||
|         this.runEntityChangeChecks("note_revision_contents", "noteRevisionId"); |         this.runEntityChangeChecks("note_revision_contents", "noteRevisionId"); | ||||||
|  |         this.runEntityChangeChecks("note_ancillaries", "noteAncillaryId"); | ||||||
|  |         this.runEntityChangeChecks("note_ancillary_contents", "noteAncillaryId"); | ||||||
|         this.runEntityChangeChecks("branches", "branchId"); |         this.runEntityChangeChecks("branches", "branchId"); | ||||||
|         this.runEntityChangeChecks("attributes", "attributeId"); |         this.runEntityChangeChecks("attributes", "attributeId"); | ||||||
|         this.runEntityChangeChecks("etapi_tokens", "etapiTokenId"); |         this.runEntityChangeChecks("etapi_tokens", "etapiTokenId"); | ||||||
| @@ -715,7 +756,7 @@ class ConsistencyChecks { | |||||||
|             return `${tableName}: ${count}`; |             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(", ")}`); |         log.info(`Table counts: ${tables.map(tableName => getTableRowCount(tableName)).join(", ")}`); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -151,6 +151,8 @@ function fillAllEntityChanges() { | |||||||
|         fillEntityChanges("branches", "branchId"); |         fillEntityChanges("branches", "branchId"); | ||||||
|         fillEntityChanges("note_revisions", "noteRevisionId"); |         fillEntityChanges("note_revisions", "noteRevisionId"); | ||||||
|         fillEntityChanges("note_revision_contents", "noteRevisionId"); |         fillEntityChanges("note_revision_contents", "noteRevisionId"); | ||||||
|  |         fillEntityChanges("note_ancillaries", "noteAncillaryId"); | ||||||
|  |         fillEntityChanges("note_ancillary_contents", "noteAncillaryId"); | ||||||
|         fillEntityChanges("attributes", "attributeId"); |         fillEntityChanges("attributes", "attributeId"); | ||||||
|         fillEntityChanges("etapi_tokens", "etapiTokenId"); |         fillEntityChanges("etapi_tokens", "etapiTokenId"); | ||||||
|         fillEntityChanges("options", "name", 'isSynced = 1'); |         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); |             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) { |         if (childBranches.length > 0) { | ||||||
|             meta.dirFileName = getUniqueFilename(existingFileNames, baseFileName); |             meta.dirFileName = getUniqueFilename(existingFileNames, baseFileName); | ||||||
|             meta.children = []; |             meta.children = []; | ||||||
| @@ -319,6 +337,16 @@ ${markdownContent}`; | |||||||
|  |  | ||||||
|         taskContext.increaseProgressCount(); |         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) { |         if (noteMeta.children && noteMeta.children.length > 0) { | ||||||
|             const directoryPath = filePathPrefix + noteMeta.dirFileName; |             const directoryPath = filePathPrefix + noteMeta.dirFileName; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ const treeService = require("../tree"); | |||||||
| const yauzl = require("yauzl"); | const yauzl = require("yauzl"); | ||||||
| const htmlSanitizer = require('../html_sanitizer'); | const htmlSanitizer = require('../html_sanitizer'); | ||||||
| const becca = require("../../becca/becca"); | const becca = require("../../becca/becca"); | ||||||
|  | const BNoteAncillary = require("../../becca/entities/bnote_ancillary"); | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @param {TaskContext} taskContext |  * @param {TaskContext} taskContext | ||||||
| @@ -64,6 +65,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) { | |||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         let parent; |         let parent; | ||||||
|  |         let ancillaryMeta = false; | ||||||
|  |  | ||||||
|         for (const segment of pathSegments) { |         for (const segment of pathSegments) { | ||||||
|             if (!cursor || !cursor.children || cursor.children.length === 0) { |             if (!cursor || !cursor.children || cursor.children.length === 0) { | ||||||
| @@ -72,11 +74,28 @@ async function importZip(taskContext, fileBuffer, importRootNote) { | |||||||
|  |  | ||||||
|             parent = cursor; |             parent = cursor; | ||||||
|             cursor = parent.children.find(file => file.dataFileName === segment || file.dirFileName === segment); |             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 { |         return { | ||||||
|             parentNoteMeta: parent, |             parentNoteMeta: parent, | ||||||
|             noteMeta: cursor |             noteMeta: cursor, | ||||||
|  |             ancillaryMeta | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -351,7 +370,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     function saveNote(filePath, content) { |     function saveNote(filePath, content) { | ||||||
|         const {parentNoteMeta, noteMeta} = getMeta(filePath); |         const {parentNoteMeta, noteMeta, ancillaryMeta} = getMeta(filePath); | ||||||
|  |  | ||||||
|         if (noteMeta?.noImport) { |         if (noteMeta?.noImport) { | ||||||
|             return; |             return; | ||||||
| @@ -359,6 +378,17 @@ async function importZip(taskContext, fileBuffer, importRootNote) { | |||||||
|  |  | ||||||
|         const noteId = getNoteId(noteMeta, filePath); |         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); |         const parentNoteId = getParentNoteId(filePath, parentNoteMeta); | ||||||
|  |  | ||||||
|         if (!parentNoteId) { |         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.`); |             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') { |             if (typeof entity.content === 'string') { | ||||||
|                 entity.content = Buffer.from(entity.content, 'UTF-8'); |                 entity.content = Buffer.from(entity.content, 'UTF-8'); | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -64,7 +64,7 @@ function updateNormalEntity(remoteEntityChange, remoteEntityRow, instanceId) { | |||||||
|         || localEntityChange.utcDateChanged < remoteEntityChange.utcDateChanged |         || localEntityChange.utcDateChanged < remoteEntityChange.utcDateChanged | ||||||
|         || localEntityChange.hash !== remoteEntityChange.hash // sync error, we should still update |         || 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); |             remoteEntityRow.content = handleContent(remoteEntityRow.content); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -115,7 +115,9 @@ function eraseEntity(entityChange, instanceId) { | |||||||
|         "branches", |         "branches", | ||||||
|         "attributes", |         "attributes", | ||||||
|         "note_revisions", |         "note_revisions", | ||||||
|         "note_revision_contents" |         "note_revision_contents", | ||||||
|  |         "note_ancillaries", | ||||||
|  |         "note_ancillary_contents" | ||||||
|     ]; |     ]; | ||||||
|  |  | ||||||
|     if (!entityNames.includes(entityName)) { |     if (!entityNames.includes(entityName)) { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user