diff --git a/src/becca/becca.js b/src/becca/becca.js index 8f11303a7..9bfa907ea 100644 --- a/src/becca/becca.js +++ b/src/becca/becca.js @@ -139,8 +139,7 @@ class Becca { /** @returns {BBlob|null} */ getBlob(blobId) { - const row = sql.getRow("SELECT *, LENGTH(content) AS contentLength " + - "FROM blob WHERE blobId = ?", [blobId]); + const row = sql.getRow("SELECT *, LENGTH(content) AS contentLength FROM blobs WHERE blobId = ?", [blobId]); const BBlob = require("./entities/bblob"); // avoiding circular dependency problems return row ? new BBlob(row) : null; diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js index b9960d41e..4b15792b0 100644 --- a/src/becca/entities/bnote.js +++ b/src/becca/entities/bnote.js @@ -212,15 +212,9 @@ class BNote extends AbstractBeccaEntity { return this._getContent(); } - /** @returns {{contentLength, dateModified, utcDateModified}} */ + /** @returns {{dateModified, utcDateModified}} */ getContentMetadata() { - return sql.getRow(` - SELECT - LENGTH(content) AS contentLength, - dateModified, - utcDateModified - FROM blobs - WHERE blobId = ?`, [this.blobId]); + return sql.getRow(`SELECT dateModified, utcDateModified FROM blobs WHERE blobId = ?`, [this.blobId]); } /** @returns {*} */ diff --git a/src/public/app/components/note_context.js b/src/public/app/components/note_context.js index 1fc68c778..16a52a7a2 100644 --- a/src/public/app/components/note_context.js +++ b/src/public/app/components/note_context.js @@ -207,6 +207,7 @@ class NoteContext extends Component { }); } + /** @returns {Promise} */ async isReadOnly() { if (this.viewScope.readOnlyTemporarilyDisabled) { return false; @@ -221,14 +222,13 @@ class NoteContext extends Component { return true; } - const noteComplement = await this.getNoteComplement(); + const blob = await this.note.getBlob(); const sizeLimit = this.note.type === 'text' ? options.getInt('autoReadonlySizeText') : options.getInt('autoReadonlySizeCode'); - return noteComplement.content - && noteComplement.content.length > sizeLimit + return blob.contentLength > sizeLimit && !this.note.hasLabel('autoReadOnlyDisabled'); } diff --git a/src/public/app/entities/fattachment.js b/src/public/app/entities/fattachment.js index e7ecf744c..becd6a24f 100644 --- a/src/public/app/entities/fattachment.js +++ b/src/public/app/entities/fattachment.js @@ -32,7 +32,7 @@ class FAttachment { } /** - * @param [opts.full=false] - force retrieval of the full note + * @param [opts.preview=false] - retrieve only first 10 000 characters for a preview * @return {FBlob} */ async getBlob(opts = {}) { diff --git a/src/public/app/entities/fblob.js b/src/public/app/entities/fblob.js index e3b0b7398..865afd53c 100644 --- a/src/public/app/entities/fblob.js +++ b/src/public/app/entities/fblob.js @@ -1,4 +1,4 @@ -class FBlob { +export default class FBlob { constructor(row) { /** @type {string} */ this.blobId = row.blobId; @@ -8,10 +8,11 @@ class FBlob { * @type {string} */ this.content = row.content; + this.contentLength = row.contentLength; /** @type {string} */ this.dateModified = row.dateModified; /** @type {string} */ this.utcDateModified = row.utcDateModified; } -} \ No newline at end of file +} diff --git a/src/public/app/entities/fnote.js b/src/public/app/entities/fnote.js index 712789d6a..47884f557 100644 --- a/src/public/app/entities/fnote.js +++ b/src/public/app/entities/fnote.js @@ -843,7 +843,7 @@ class FNote { /** * Get relations which target this note * - * @returns {FNote[]} + * @returns {Promise} */ async getTargetRelationSourceNotes() { const targetRelations = this.getTargetRelations(); @@ -851,14 +851,17 @@ class FNote { return await this.froca.getNotes(targetRelations.map(tr => tr.noteId)); } - /** @deprecated use getBlob() instead */ + /** + * @deprecated use getBlob() instead + * @return {Promise} + */ async getNoteComplement() { - return this.getBlob({ full: true }); + return this.getBlob(); } /** - * @param [opts.full=false] - force retrieval of the full note - * @return {FBlob} + * @param [opts.preview=false] - retrieve only first 10 000 characters for a preview + * @return {Promise} */ async getBlob(opts = {}) { return await this.froca.getBlob('notes', this.noteId, opts); diff --git a/src/public/app/services/froca.js b/src/public/app/services/froca.js index f20da91a4..929f4f5c0 100644 --- a/src/public/app/services/froca.js +++ b/src/public/app/services/froca.js @@ -320,12 +320,13 @@ class Froca { return attachmentRow ? new FAttachment(this, attachmentRow) : null; } + /** @returns {Promise} */ async getBlob(entityType, entityId, opts = {}) { - opts.full = !!opts.full; - const key = `${entityType}-${entityId}`; + opts.preview = !!opts.preview; + const key = `${entityType}-${entityId}-${opts.preview}`; if (!this.blobPromises[key]) { - this.blobPromises[key] = server.get(`${entityType}/${entityId}/blob?full=${opts.full}`) + this.blobPromises[key] = server.get(`${entityType}/${entityId}/blob?preview=${opts.preview}`) .then(row => new FBlob(row)) .catch(e => console.error(`Cannot get blob for ${entityType} '${entityId}'`)); diff --git a/src/public/app/services/note_content_renderer.js b/src/public/app/services/note_content_renderer.js index 2fb805799..a870bb132 100644 --- a/src/public/app/services/note_content_renderer.js +++ b/src/public/app/services/note_content_renderer.js @@ -11,6 +11,11 @@ import treeService from "./tree.js"; let idCounter = 1; +/** + * @param {FNote} note + * @param {object} options + * @return {Promise<{type: string, $renderedContent: jQuery}>} + */ async function getRenderedContent(note, options = {}) { options = Object.assign({ trim: false, @@ -22,10 +27,10 @@ async function getRenderedContent(note, options = {}) { const $renderedContent = $('
'); if (type === 'text') { - const noteComplement = await froca.getNoteComplement(note.noteId); + const blob = await note.getBlob({ preview: options.trim }); - if (!utils.isHtmlEmpty(noteComplement.content)) { - $renderedContent.append($('
').html(trim(noteComplement.content, options.trim))); + if (!utils.isHtmlEmpty(blob.content)) { + $renderedContent.append($('
').html(trim(blob.content, options.trim))); if ($renderedContent.find('span.math-tex').length > 0) { await libraryLoader.requireLibrary(libraryLoader.KATEX); @@ -108,8 +113,8 @@ async function getRenderedContent(note, options = {}) { else if (type === 'mermaid') { await libraryLoader.requireLibrary(libraryLoader.MERMAID); - const noteComplement = await froca.getNoteComplement(note.noteId); - const content = noteComplement.content || ""; + const blob = await note.getBlob(); + const content = blob.content || ""; $renderedContent .css("display", "flex") @@ -140,8 +145,8 @@ async function getRenderedContent(note, options = {}) { // make sure surrounding container has size of what is visible. Then image is shrinked to its boundaries $renderedContent.css({height: "100%", width:"100%"}); - const noteComplement = await froca.getNoteComplement(note.noteId); - const content = noteComplement.content || ""; + const blob = await note.getBlob(); + const content = blob.content || ""; try { const placeHolderSVG = ""; diff --git a/src/public/app/widgets/mermaid.js b/src/public/app/widgets/mermaid.js index 857eca97e..305209199 100644 --- a/src/public/app/widgets/mermaid.js +++ b/src/public/app/widgets/mermaid.js @@ -99,8 +99,8 @@ export default class MermaidWidget extends NoteContextAwareWidget { async renderSvg(cb) { idCounter++; - const noteComplement = await froca.getNoteComplement(this.noteId); - const content = noteComplement.content || ""; + const blob = await this.note.getBlob(); + const content = blob.content || ""; // this can't be promisified since in case of error this both calls callback with error SVG and throws exception // with error details diff --git a/src/public/app/widgets/note_context_aware_widget.js b/src/public/app/widgets/note_context_aware_widget.js index bc1b72180..fe6ebe434 100644 --- a/src/public/app/widgets/note_context_aware_widget.js +++ b/src/public/app/widgets/note_context_aware_widget.js @@ -19,18 +19,22 @@ export default class NoteContextAwareWidget extends BasicWidget { return this.noteId === noteId; } + /** @returns {FNote|undefined} */ get note() { return this.noteContext?.note; } + /** @returns {string|undefined} */ get noteId() { return this.note?.noteId; } + /** @returns {string|undefined} */ get notePath() { return this.noteContext?.notePath; } + /** @returns {string} */ get hoistedNoteId() { return this.noteContext?.hoistedNoteId; } diff --git a/src/public/app/widgets/note_type.js b/src/public/app/widgets/note_type.js index 708375d61..ab39234e9 100644 --- a/src/public/app/widgets/note_type.js +++ b/src/public/app/widgets/note_type.js @@ -143,9 +143,9 @@ export default class NoteTypeWidget extends NoteContextAwareWidget { } async confirmChangeIfContent() { - const noteComplement = await this.noteContext.getNoteComplement(); + const blob = await this.note.getBlob(); - if (!noteComplement.content || !noteComplement.content.trim().length) { + if (!blob.content || !blob.content.trim().length) { return true; } diff --git a/src/public/app/widgets/ribbon_widgets/file_properties.js b/src/public/app/widgets/ribbon_widgets/file_properties.js index a7e4e06df..fb4c91fcc 100644 --- a/src/public/app/widgets/ribbon_widgets/file_properties.js +++ b/src/public/app/widgets/ribbon_widgets/file_properties.js @@ -134,9 +134,9 @@ export default class FilePropertiesWidget extends NoteContextAwareWidget { this.$fileName.text(attributeMap.originalFileName || "?"); this.$fileType.text(note.mime); - const noteComplement = await this.noteContext.getNoteComplement(); + const blob = await this.note.getBlob(); - this.$fileSize.text(`${noteComplement.contentLength} bytes`); + this.$fileSize.text(`${blob.contentLength} bytes`); // open doesn't work for protected notes since it works through browser which isn't in protected session this.$openButton.toggle(!note.isProtected); diff --git a/src/public/app/widgets/ribbon_widgets/image_properties.js b/src/public/app/widgets/ribbon_widgets/image_properties.js index 22fd5f9a6..db10ecafa 100644 --- a/src/public/app/widgets/ribbon_widgets/image_properties.js +++ b/src/public/app/widgets/ribbon_widgets/image_properties.js @@ -116,10 +116,10 @@ export default class ImagePropertiesWidget extends NoteContextAwareWidget { this.$widget.show(); - const noteComplement = await this.noteContext.getNoteComplement(); + const blob = await this.note.getBlob(); this.$fileName.text(attributeMap.originalFileName || "?"); - this.$fileSize.text(`${noteComplement.contentLength} bytes`); + this.$fileSize.text(`${blob.contentLength} bytes`); this.$fileType.text(note.mime); } } diff --git a/src/public/app/widgets/ribbon_widgets/note_info_widget.js b/src/public/app/widgets/ribbon_widgets/note_info_widget.js index c463c9c0d..2416ffe3d 100644 --- a/src/public/app/widgets/ribbon_widgets/note_info_widget.js +++ b/src/public/app/widgets/ribbon_widgets/note_info_widget.js @@ -120,23 +120,22 @@ export default class NoteInfoWidget extends NoteContextAwareWidget { } async refreshWithNote(note) { - const noteComplement = await this.noteContext.getNoteComplement(); + const metadata = await server.get(`notes/${this.noteId}/metadata`); this.$noteId.text(note.noteId); this.$dateCreated - .text(noteComplement.dateCreated.substr(0, 16)) - .attr("title", noteComplement.dateCreated); + .text(metadata.dateCreated.substr(0, 16)) + .attr("title", metadata.dateCreated); this.$dateModified - .text(noteComplement.combinedDateModified.substr(0, 16)) - .attr("title", noteComplement.combinedDateModified); + .text(metadata.combinedDateModified.substr(0, 16)) + .attr("title", metadata.combinedDateModified); this.$type.text(note.type); if (note.mime) { this.$mime.text(`(${note.mime})`); - } - else { + } else { this.$mime.empty(); } diff --git a/src/public/app/widgets/type_widgets/canvas.js b/src/public/app/widgets/type_widgets/canvas.js index dbe1eba2e..365b89bf7 100644 --- a/src/public/app/widgets/type_widgets/canvas.js +++ b/src/public/app/widgets/type_widgets/canvas.js @@ -156,7 +156,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget { this.currentNoteId = note.noteId; // get note from backend and put into canvas - const noteComplement = await froca.getNoteComplement(note.noteId); + const blob = await note.getBlob(); // before we load content into excalidraw, make sure excalidraw has loaded while (!this.excalidrawRef || !this.excalidrawRef.current) { @@ -170,7 +170,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget { * note into this fresh note. Probably due to that this note-instance does not get * newly instantiated? */ - if (this.excalidrawRef.current && noteComplement.content?.trim() === "") { + if (this.excalidrawRef.current && blob.content?.trim() === "") { const sceneData = { elements: [], appState: { @@ -181,16 +181,16 @@ export default class ExcalidrawTypeWidget extends TypeWidget { this.excalidrawRef.current.updateScene(sceneData); } - else if (this.excalidrawRef.current && noteComplement.content) { + else if (this.excalidrawRef.current && blob.content) { // load saved content into excalidraw canvas let content; try { - content = JSON.parse(noteComplement.content || ""); + content = JSON.parse(blob.content || ""); } catch(err) { console.error("Error parsing content. Probably note.type changed", "Starting with empty canvas" - , note, noteComplement, err); + , note, blob, err); content = { elements: [], diff --git a/src/public/app/widgets/type_widgets/editable_code.js b/src/public/app/widgets/type_widgets/editable_code.js index a183601ce..00a969bc4 100644 --- a/src/public/app/widgets/type_widgets/editable_code.js +++ b/src/public/app/widgets/type_widgets/editable_code.js @@ -69,12 +69,12 @@ export default class EditableCodeTypeWidget extends TypeWidget { } async doRefresh(note) { - const noteComplement = await this.noteContext.getNoteComplement(); + const blob = await this.note.getBlob(); await this.spacedUpdate.allowUpdateWithoutChange(() => { // CodeMirror breaks pretty badly on null so even though it shouldn't happen (guarded by consistency check) // we provide fallback - this.codeEditor.setValue(noteComplement.content || ""); + this.codeEditor.setValue(blob.content || ""); this.codeEditor.clearHistory(); let info = CodeMirror.findModeByMIME(note.mime); diff --git a/src/public/app/widgets/type_widgets/editable_text.js b/src/public/app/widgets/type_widgets/editable_text.js index 2c6cf55f2..f905b4344 100644 --- a/src/public/app/widgets/type_widgets/editable_text.js +++ b/src/public/app/widgets/type_widgets/editable_text.js @@ -183,10 +183,10 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { } async doRefresh(note) { - const noteComplement = await froca.getNoteComplement(note.noteId); + const blob = await note.getBlob(); await this.spacedUpdate.allowUpdateWithoutChange(() => { - this.watchdog.editor.setData(noteComplement.content || ""); + this.watchdog.editor.setData(blob.content || ""); }); } diff --git a/src/public/app/widgets/type_widgets/file.js b/src/public/app/widgets/type_widgets/file.js index 0fd78bd35..b3e35265a 100644 --- a/src/public/app/widgets/type_widgets/file.js +++ b/src/public/app/widgets/type_widgets/file.js @@ -52,7 +52,7 @@ export default class FileTypeWidget extends TypeWidget { async doRefresh(note) { this.$widget.show(); - const noteComplement = await this.noteContext.getNoteComplement(); + const blob = await this.note.getBlob(); this.$previewContent.empty().hide(); this.$pdfPreview.attr('src', '').empty().hide(); @@ -60,9 +60,9 @@ export default class FileTypeWidget extends TypeWidget { this.$videoPreview.hide(); this.$audioPreview.hide(); - if (noteComplement.content) { + if (blob.content) { this.$previewContent.show().scrollTop(0); - this.$previewContent.text(noteComplement.content); + this.$previewContent.text(blob.content); } else if (note.mime === 'application/pdf') { this.$pdfPreview.show().attr("src", openService.getUrlForDownload(`api/notes/${this.noteId}/open`)); diff --git a/src/public/app/widgets/type_widgets/read_only_code.js b/src/public/app/widgets/type_widgets/read_only_code.js index bd817fa65..39a76c245 100644 --- a/src/public/app/widgets/type_widgets/read_only_code.js +++ b/src/public/app/widgets/type_widgets/read_only_code.js @@ -27,9 +27,9 @@ export default class ReadOnlyCodeTypeWidget extends TypeWidget { } async doRefresh(note) { - const noteComplement = await this.noteContext.getNoteComplement(); + const blob = await this.note.getBlob(); - this.$content.text(noteComplement.content); + this.$content.text(blob.content); } async executeWithContentElementEvent({resolve, ntxId}) { diff --git a/src/public/app/widgets/type_widgets/read_only_text.js b/src/public/app/widgets/type_widgets/read_only_text.js index ff02138db..aa2eac47d 100644 --- a/src/public/app/widgets/type_widgets/read_only_text.js +++ b/src/public/app/widgets/type_widgets/read_only_text.js @@ -93,9 +93,9 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget { // (see https://github.com/zadam/trilium/issues/1590 for example of such conflict) await libraryLoader.requireLibrary(libraryLoader.CKEDITOR); - const noteComplement = await froca.getNoteComplement(note.noteId); + const blob = await note.getBlob(); - this.$content.html(noteComplement.content); + this.$content.html(blob.content); this.$content.find("a.reference-link").each(async (_, el) => { const notePath = $(el).attr('href'); diff --git a/src/public/app/widgets/type_widgets/relation_map.js b/src/public/app/widgets/type_widgets/relation_map.js index c8619d8a1..82dfb895e 100644 --- a/src/public/app/widgets/type_widgets/relation_map.js +++ b/src/public/app/widgets/type_widgets/relation_map.js @@ -197,11 +197,11 @@ export default class RelationMapTypeWidget extends TypeWidget { } }; - const noteComplement = await this.noteContext.getNoteComplement(); + const blob = await this.note.getBlob(); - if (noteComplement.content) { + if (blob.content) { try { - this.mapData = JSON.parse(noteComplement.content); + this.mapData = JSON.parse(blob.content); } catch (e) { console.log("Could not parse content: ", e); } diff --git a/src/routes/api/attachments.js b/src/routes/api/attachments.js index f30be9632..424740e2f 100644 --- a/src/routes/api/attachments.js +++ b/src/routes/api/attachments.js @@ -4,9 +4,9 @@ const utils = require("../../services/utils"); const blobService = require("../../services/blob.js"); function getAttachmentBlob(req) { - const full = req.query.full === 'true'; + const preview = req.query.preview === 'true'; - return blobService.getBlobPojo('attachments', req.params.attachmentId, { full }); + return blobService.getBlobPojo('attachments', req.params.attachmentId, { preview }); } function getAttachments(req) { diff --git a/src/routes/api/note_revisions.js b/src/routes/api/note_revisions.js index 0749d845b..abc9415c6 100644 --- a/src/routes/api/note_revisions.js +++ b/src/routes/api/note_revisions.js @@ -10,9 +10,9 @@ const becca = require("../../becca/becca"); const blobService = require("../../services/blob.js"); function getNoteRevisionBlob(req) { - const full = req.query.full === 'true'; + const preview = req.query.preview === 'true'; - return blobService.getBlobPojo('note_revisions', req.params.noteRevisionId, { full }); + return blobService.getBlobPojo('note_revisions', req.params.noteRevisionId, { preview }); } function getNoteRevisions(req) { diff --git a/src/routes/api/notes.js b/src/routes/api/notes.js index 586721235..4a67d1112 100644 --- a/src/routes/api/notes.js +++ b/src/routes/api/notes.js @@ -6,7 +6,6 @@ const sql = require('../../services/sql'); const utils = require('../../services/utils'); const log = require('../../services/log'); const TaskContext = require('../../services/task_context'); -const fs = require('fs'); const becca = require("../../becca/becca"); const ValidationError = require("../../errors/validation_error"); const NotFoundError = require("../../errors/not_found_error"); @@ -18,31 +17,27 @@ function getNote(req) { throw new NotFoundError(`Note '${req.params.noteId}' has not been found.`); } - const pojo = note.getPojo(); + return note; +} - if (note.hasStringContent()) { - pojo.content = note.getContent(); +function getNoteBlob(req) { + const preview = req.query.preview === 'true'; - // FIXME: use blobs instead - if (note.type === 'file' && pojo.content.length > 10000) { - pojo.content = `${pojo.content.substr(0, 10000)}\r\n\r\n... and ${pojo.content.length - 10000} more characters.`; - } + return blobService.getBlobPojo('notes', req.params.noteId, { preview }); +} + +function getNoteMetadata(req) { + const note = becca.getNote(req.params.noteId); + if (!note) { + throw new NotFoundError(`Note '${req.params.noteId}' has not been found.`); } const contentMetadata = note.getContentMetadata(); - pojo.contentLength = contentMetadata.contentLength; - - pojo.combinedUtcDateModified = note.utcDateModified > contentMetadata.utcDateModified ? note.utcDateModified : contentMetadata.utcDateModified; - pojo.combinedDateModified = note.utcDateModified > contentMetadata.utcDateModified ? note.dateModified : contentMetadata.dateModified; - - return pojo; -} - -function getNoteBlob(req) { - const full = req.query.full === 'true'; - - return blobService.getBlobPojo('notes', req.params.noteId, { full }); + return { + dateCreated: note.dateCreated, + combinedDateModified: note.utcDateModified > contentMetadata.utcDateModified ? note.dateModified : contentMetadata.dateModified + }; } function createNote(req) { @@ -266,6 +261,7 @@ function convertNoteToAttachment(req) { module.exports = { getNote, getNoteBlob, + getNoteMetadata, updateNoteData, deleteNote, undeleteNote, diff --git a/src/routes/routes.js b/src/routes/routes.js index b5566d24a..f84a58371 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -113,6 +113,7 @@ function register(app) { apiRoute(GET, '/api/notes/:noteId', notesApiRoute.getNote); apiRoute(GET, '/api/notes/:noteId/blob', notesApiRoute.getNoteBlob); + apiRoute(GET, '/api/notes/:noteId/metadata', notesApiRoute.getNoteMetadata); apiRoute(PUT, '/api/notes/:noteId/data', notesApiRoute.updateNoteData); apiRoute(DEL, '/api/notes/:noteId', notesApiRoute.deleteNote); apiRoute(PUT, '/api/notes/:noteId/undelete', notesApiRoute.undeleteNote); diff --git a/src/services/blob.js b/src/services/blob.js index dd3715b90..e85d7ae5b 100644 --- a/src/services/blob.js +++ b/src/services/blob.js @@ -2,7 +2,7 @@ const becca = require('../becca/becca'); const NotFoundError = require("../errors/not_found_error"); function getBlobPojo(entityName, entityId, opts = {}) { - opts.full = !!opts.full; + opts.preview = !!opts.preview; const entity = becca.getEntity(entityName, entityId); @@ -16,7 +16,7 @@ function getBlobPojo(entityName, entityId, opts = {}) { if (!entity.hasStringContent()) { pojo.content = null; - } else if (!opts.full && pojo.content.length > 10000) { + } else if (opts.preview && pojo.content.length > 10000) { pojo.content = `${pojo.content.substr(0, 10000)}\r\n\r\n... and ${pojo.content.length - 10000} more characters.`; } @@ -25,4 +25,4 @@ function getBlobPojo(entityName, entityId, opts = {}) { module.exports = { getBlobPojo -}; \ No newline at end of file +};