From 5e1f81e53e4c75f08863de3a32468d9b5b11f5ef Mon Sep 17 00:00:00 2001 From: zadam Date: Fri, 5 May 2023 16:37:39 +0200 Subject: [PATCH] getting rid of note complement WIP --- src/becca/becca.js | 11 ++++++ src/becca/entities/abstract_becca_entity.js | 4 +- src/becca/entities/battachment.js | 2 +- src/becca/entities/bblob.js | 26 +++++++++++++ src/becca/entities/bbranch.js | 1 + src/becca/entities/bnote.js | 7 +++- src/becca/entities/bnote_revision.js | 2 +- src/public/app/components/note_context.js | 9 ----- src/public/app/entities/fattachment.js | 8 ++++ src/public/app/entities/fblob.js | 17 +++++++++ src/public/app/entities/fnote.js | 16 +++++--- src/public/app/entities/fnote_complement.js | 41 --------------------- src/public/app/services/froca.js | 28 +++++++------- src/public/app/widgets/toc.js | 2 +- src/routes/api/attachments.js | 8 ++++ src/routes/api/note_revisions.js | 12 +++++- src/routes/api/notes.js | 17 ++++++--- src/routes/routes.js | 3 ++ src/services/blob.js | 28 ++++++++++++++ src/services/export/opml.js | 4 +- src/services/handlers.js | 2 +- src/services/notes.js | 1 - src/share/shaca/entities/snote.js | 4 +- 23 files changed, 163 insertions(+), 90 deletions(-) create mode 100644 src/becca/entities/bblob.js create mode 100644 src/public/app/entities/fblob.js delete mode 100644 src/public/app/entities/fnote_complement.js create mode 100644 src/services/blob.js diff --git a/src/becca/becca.js b/src/becca/becca.js index 4e63dc7e5..8f11303a7 100644 --- a/src/becca/becca.js +++ b/src/becca/becca.js @@ -137,6 +137,15 @@ class Becca { .map(row => new BAttachment(row)); } + /** @returns {BBlob|null} */ + getBlob(blobId) { + const row = sql.getRow("SELECT *, LENGTH(content) AS contentLength " + + "FROM blob WHERE blobId = ?", [blobId]); + + const BBlob = require("./entities/bblob"); // avoiding circular dependency problems + return row ? new BBlob(row) : null; + } + /** @returns {BOption|null} */ getOption(name) { return this.options[name]; @@ -161,6 +170,8 @@ class Becca { return this.getNoteRevision(entityId); } else if (entityName === 'attachments') { return this.getAttachment(entityId); + } else if (entityName === 'blobs') { + return this.getBlob(entityId); } const camelCaseEntityName = entityName.toLowerCase().replace(/(_[a-z])/g, diff --git a/src/becca/entities/abstract_becca_entity.js b/src/becca/entities/abstract_becca_entity.js index 5e49b7c93..0fbf01630 100644 --- a/src/becca/entities/abstract_becca_entity.js +++ b/src/becca/entities/abstract_becca_entity.js @@ -142,7 +142,7 @@ class AbstractBeccaEntity { throw new Error(`Cannot set null content to ${this.constructor.primaryKeyName} '${this[this.constructor.primaryKeyName]}'`); } - if (this.isStringNote()) { + if (this.hasStringContent()) { content = content.toString(); } else { @@ -246,7 +246,7 @@ class AbstractBeccaEntity { } } - if (this.isStringNote()) { + if (this.hasStringContent()) { return content === null ? "" : content.toString("UTF-8"); diff --git a/src/becca/entities/battachment.js b/src/becca/entities/battachment.js index adc84380a..fd303542a 100644 --- a/src/becca/entities/battachment.js +++ b/src/becca/entities/battachment.js @@ -76,7 +76,7 @@ class BAttachment extends AbstractBeccaEntity { } /** @returns {boolean} true if the note has string content (not binary) */ - isStringNote() { + hasStringContent() { return utils.isStringNote(this.type, this.mime); } diff --git a/src/becca/entities/bblob.js b/src/becca/entities/bblob.js new file mode 100644 index 000000000..2af26cb40 --- /dev/null +++ b/src/becca/entities/bblob.js @@ -0,0 +1,26 @@ +class BBlob { + constructor(row) { + /** @type {string} */ + this.blobId = row.blobId; + /** @type {string|Buffer} */ + this.content = row.content; + /** @type {number} */ + this.contentLength = row.contentLength; + /** @type {string} */ + this.dateModified = row.dateModified; + /** @type {string} */ + this.utcDateModified = row.utcDateModified; + } + + getPojo() { + return { + blobId: this.blobId, + content: this.content, + contentLength: this.contentLength, + dateModified: this.dateModified, + utcDateModified: this.utcDateModified + }; + } +} + +module.exports = BBlob; \ No newline at end of file diff --git a/src/becca/entities/bbranch.js b/src/becca/entities/bbranch.js index 38290e4ab..cfd855d47 100644 --- a/src/becca/entities/bbranch.js +++ b/src/becca/entities/bbranch.js @@ -103,6 +103,7 @@ class BBranch extends AbstractBeccaEntity { return this.becca.notes[this.noteId]; } + /** @returns {BNote} */ getNote() { return this.childNote; } diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js index 60ee4dd3f..b9960d41e 100644 --- a/src/becca/entities/bnote.js +++ b/src/becca/entities/bnote.js @@ -301,8 +301,13 @@ class BNote extends AbstractBeccaEntity { || (this.type === 'file' && this.mime?.startsWith('image/')); } - /** @returns {boolean} true if the note has string content (not binary) */ + /** @deprecated use hasStringContent() instead */ isStringNote() { + return this.hasStringContent(); + } + + /** @returns {boolean} true if the note has string content (not binary) */ + hasStringContent() { return utils.isStringNote(this.type, this.mime); } diff --git a/src/becca/entities/bnote_revision.js b/src/becca/entities/bnote_revision.js index 1e127c4a5..0167d22a5 100644 --- a/src/becca/entities/bnote_revision.js +++ b/src/becca/entities/bnote_revision.js @@ -61,7 +61,7 @@ class BNoteRevision extends AbstractBeccaEntity { } /** @returns {boolean} true if the note has string content (not binary) */ - isStringNote() { + hasStringContent() { return utils.isStringNote(this.type, this.mime); } diff --git a/src/public/app/components/note_context.js b/src/public/app/components/note_context.js index 4284410d7..1fc68c778 100644 --- a/src/public/app/components/note_context.js +++ b/src/public/app/components/note_context.js @@ -166,15 +166,6 @@ class NoteContext extends Component { return this.notePath ? this.notePath.split('/') : []; } - /** @returns {FNoteComplement} */ - async getNoteComplement() { - if (!this.noteId) { - return null; - } - - return await froca.getNoteComplement(this.noteId); - } - isActive() { return appContext.tabManager.activeNtxId === this.ntxId; } diff --git a/src/public/app/entities/fattachment.js b/src/public/app/entities/fattachment.js index baa017023..e7ecf744c 100644 --- a/src/public/app/entities/fattachment.js +++ b/src/public/app/entities/fattachment.js @@ -30,6 +30,14 @@ class FAttachment { getNote() { return this.froca.notes[this.parentId]; } + + /** + * @param [opts.full=false] - force retrieval of the full note + * @return {FBlob} + */ + async getBlob(opts = {}) { + return await this.froca.getBlob('attachments', this.attachmentId, opts); + } } export default FAttachment; diff --git a/src/public/app/entities/fblob.js b/src/public/app/entities/fblob.js new file mode 100644 index 000000000..e3b0b7398 --- /dev/null +++ b/src/public/app/entities/fblob.js @@ -0,0 +1,17 @@ +class FBlob { + constructor(row) { + /** @type {string} */ + this.blobId = row.blobId; + + /** + * can either contain the whole content (in e.g. string notes), only part (large text notes) or nothing at all (binary notes, images) + * @type {string} + */ + this.content = row.content; + + /** @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 9f942cd95..712789d6a 100644 --- a/src/public/app/entities/fnote.js +++ b/src/public/app/entities/fnote.js @@ -851,13 +851,17 @@ class FNote { return await this.froca.getNotes(targetRelations.map(tr => tr.noteId)); } - /** - * Return note complement which is most importantly note's content - * - * @returns {Promise} - */ + /** @deprecated use getBlob() instead */ async getNoteComplement() { - return await this.froca.getNoteComplement(this.noteId); + return this.getBlob({ full: true }); + } + + /** + * @param [opts.full=false] - force retrieval of the full note + * @return {FBlob} + */ + async getBlob(opts = {}) { + return await this.froca.getBlob('notes', this.noteId, opts); } toString() { diff --git a/src/public/app/entities/fnote_complement.js b/src/public/app/entities/fnote_complement.js deleted file mode 100644 index 39253afcd..000000000 --- a/src/public/app/entities/fnote_complement.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * FIXME: probably make it a FBlob - * Complements the FNote with the main note content and other extra attributes - */ -class FNoteComplement { - constructor(row) { - /** @type {string} */ - this.noteId = row.noteId; - - /** - * can either contain the whole content (in e.g. string notes), only part (large text notes) or nothing at all (binary notes, images) - * @type {string} - */ - this.content = row.content; - - /** @type {int} */ - this.contentLength = row.contentLength; - - /** @type {string} */ - this.dateCreated = row.dateCreated; - - /** @type {string} */ - this.dateModified = row.dateModified; - - /** @type {string} */ - this.utcDateCreated = row.utcDateCreated; - - /** @type {string} */ - this.utcDateModified = row.utcDateModified; - - // "combined" date modified give larger out of note's and blob's dateModified - - /** @type {string} */ - this.combinedDateModified = row.combinedDateModified; - - /** @type {string} */ - this.combinedUtcDateModified = row.combinedUtcDateModified; - } -} - -export default FNoteComplement; diff --git a/src/public/app/services/froca.js b/src/public/app/services/froca.js index 3e67f583b..f20da91a4 100644 --- a/src/public/app/services/froca.js +++ b/src/public/app/services/froca.js @@ -3,7 +3,7 @@ import FNote from "../entities/fnote.js"; import FAttribute from "../entities/fattribute.js"; import server from "./server.js"; import appContext from "../components/app_context.js"; -import FNoteComplement from "../entities/fnote_complement.js"; +import FBlob from "../entities/fblob.js"; import FAttachment from "../entities/fattachment.js"; /** @@ -38,8 +38,7 @@ class Froca { /** @type {Object.} */ this.attachments = {}; - // FIXME - /** @type {Object.>} */ + /** @type {Object.>} */ this.blobPromises = {}; this.addResp(resp); @@ -321,25 +320,24 @@ class Froca { return attachmentRow ? new FAttachment(this, attachmentRow) : null; } - /** - * // FIXME - * @returns {Promise} - */ - async getNoteComplement(noteId) { - if (!this.blobPromises[noteId]) { - this.blobPromises[noteId] = server.get(`notes/${noteId}`) - .then(row => new FNoteComplement(row)) - .catch(e => console.error(`Cannot get note complement for note '${noteId}'`)); + async getBlob(entityType, entityId, opts = {}) { + opts.full = !!opts.full; + const key = `${entityType}-${entityId}`; + + if (!this.blobPromises[key]) { + this.blobPromises[key] = server.get(`${entityType}/${entityId}/blob?full=${opts.full}`) + .then(row => new FBlob(row)) + .catch(e => console.error(`Cannot get blob for ${entityType} '${entityId}'`)); // we don't want to keep large payloads forever in memory, so we clean that up quite quickly // this cache is more meant to share the data between different components within one business transaction (e.g. loading of the note into the tab context and all the components) // this is also a workaround for missing invalidation after change - this.blobPromises[noteId].then( - () => setTimeout(() => this.blobPromises[noteId] = null, 1000) + this.blobPromises[key].then( + () => setTimeout(() => this.blobPromises[key] = null, 1000) ); } - return await this.blobPromises[noteId]; + return await this.blobPromises[key]; } } diff --git a/src/public/app/widgets/toc.js b/src/public/app/widgets/toc.js index 3efb880ed..4840a6310 100644 --- a/src/public/app/widgets/toc.js +++ b/src/public/app/widgets/toc.js @@ -90,7 +90,7 @@ export default class TocWidget extends RightPanelWidget { let $toc = "", headingCount = 0; // Check for type text unconditionally in case alwaysShowWidget is set if (this.note.type === 'text') { - const { content } = await note.getNoteComplement(); + const { content } = await note.getBlob(); ({$toc, headingCount} = await this.getToc(content)); } diff --git a/src/routes/api/attachments.js b/src/routes/api/attachments.js index 2eebaa4d3..f30be9632 100644 --- a/src/routes/api/attachments.js +++ b/src/routes/api/attachments.js @@ -1,6 +1,13 @@ const becca = require("../../becca/becca"); const NotFoundError = require("../../errors/not_found_error"); const utils = require("../../services/utils"); +const blobService = require("../../services/blob.js"); + +function getAttachmentBlob(req) { + const full = req.query.full === 'true'; + + return blobService.getBlobPojo('attachments', req.params.attachmentId, { full }); +} function getAttachments(req) { const includeContent = req.query.includeContent === 'true'; @@ -87,6 +94,7 @@ function convertAttachmentToNote(req) { } module.exports = { + getAttachmentBlob, getAttachments, getAttachment, saveAttachment, diff --git a/src/routes/api/note_revisions.js b/src/routes/api/note_revisions.js index f1a919aaa..0749d845b 100644 --- a/src/routes/api/note_revisions.js +++ b/src/routes/api/note_revisions.js @@ -1,13 +1,19 @@ "use strict"; const beccaService = require('../../becca/becca_service'); -const protectedSessionService = require('../../services/protected_session'); const noteRevisionService = require('../../services/note_revisions'); const utils = require('../../services/utils'); const sql = require('../../services/sql'); const cls = require('../../services/cls'); const path = require('path'); const becca = require("../../becca/becca"); +const blobService = require("../../services/blob.js"); + +function getNoteRevisionBlob(req) { + const full = req.query.full === 'true'; + + return blobService.getBlobPojo('note_revisions', req.params.noteRevisionId, { full }); +} function getNoteRevisions(req) { return becca.getNoteRevisionsFromQuery(` @@ -20,10 +26,11 @@ function getNoteRevisions(req) { } function getNoteRevision(req) { + // FIXME const noteRevision = becca.getNoteRevision(req.params.noteRevisionId); if (noteRevision.type === 'file') { - if (noteRevision.isStringNote()) { + if (noteRevision.hasStringContent()) { noteRevision.content = noteRevision.getContent().substr(0, 10000); } } @@ -180,6 +187,7 @@ function getNotePathData(note) { } module.exports = { + getNoteRevisionBlob, getNoteRevisions, getNoteRevision, downloadNoteRevision, diff --git a/src/routes/api/notes.js b/src/routes/api/notes.js index 97e81411d..586721235 100644 --- a/src/routes/api/notes.js +++ b/src/routes/api/notes.js @@ -10,20 +10,20 @@ const fs = require('fs'); const becca = require("../../becca/becca"); const ValidationError = require("../../errors/validation_error"); const NotFoundError = require("../../errors/not_found_error"); +const blobService = require("../../services/blob"); function getNote(req) { - const noteId = req.params.noteId; - const note = becca.getNote(noteId); - + const note = becca.getNote(req.params.noteId); if (!note) { - throw new NotFoundError(`Note '${noteId}' has not been found.`); + throw new NotFoundError(`Note '${req.params.noteId}' has not been found.`); } const pojo = note.getPojo(); - if (note.isStringNote()) { + if (note.hasStringContent()) { pojo.content = note.getContent(); + // 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.`; } @@ -39,6 +39,12 @@ function getNote(req) { return pojo; } +function getNoteBlob(req) { + const full = req.query.full === 'true'; + + return blobService.getBlobPojo('notes', req.params.noteId, { full }); +} + function createNote(req) { const params = Object.assign({}, req.body); // clone params.parentNoteId = req.params.parentNoteId; @@ -259,6 +265,7 @@ function convertNoteToAttachment(req) { module.exports = { getNote, + getNoteBlob, updateNoteData, deleteNote, undeleteNote, diff --git a/src/routes/routes.js b/src/routes/routes.js index 0ac273a00..b5566d24a 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -112,6 +112,7 @@ function register(app) { apiRoute(PST, '/api/tree/load', treeApiRoute.load); apiRoute(GET, '/api/notes/:noteId', notesApiRoute.getNote); + apiRoute(GET, '/api/notes/:noteId/blob', notesApiRoute.getNoteBlob); apiRoute(PUT, '/api/notes/:noteId/data', notesApiRoute.updateNoteData); apiRoute(DEL, '/api/notes/:noteId', notesApiRoute.deleteNote); apiRoute(PUT, '/api/notes/:noteId/undelete', notesApiRoute.undeleteNote); @@ -153,6 +154,7 @@ function register(app) { apiRoute(GET, '/api/attachments/:attachmentId', attachmentsApiRoute.getAttachment); apiRoute(PST, '/api/attachments/:attachmentId/convert-to-note', attachmentsApiRoute.convertAttachmentToNote); apiRoute(DEL, '/api/attachments/:attachmentId', attachmentsApiRoute.deleteAttachment); + apiRoute(GET, '/api/attachments/:attachmentId/blob', attachmentsApiRoute.getAttachmentBlob); route(GET, '/api/attachments/:attachmentId/image/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnAttachedImage); route(GET, '/api/attachments/:attachmentId/open', [auth.checkApiAuthOrElectron], filesRoute.openAttachment); route(GET, '/api/attachments/:attachmentId/open-partial', [auth.checkApiAuthOrElectron], @@ -170,6 +172,7 @@ function register(app) { apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions); apiRoute(DEL, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.eraseAllNoteRevisions); apiRoute(GET, '/api/revisions/:noteRevisionId', noteRevisionsApiRoute.getNoteRevision); + apiRoute(GET, '/api/revisions/:noteRevisionId/blob', noteRevisionsApiRoute.getNoteRevisionBlob); apiRoute(DEL, '/api/revisions/:noteRevisionId', noteRevisionsApiRoute.eraseNoteRevision); apiRoute(PST, '/api/revisions/:noteRevisionId/restore', noteRevisionsApiRoute.restoreNoteRevision); route(GET, '/api/revisions/:noteRevisionId/download', [auth.checkApiAuthOrElectron], noteRevisionsApiRoute.downloadNoteRevision); diff --git a/src/services/blob.js b/src/services/blob.js new file mode 100644 index 000000000..dd3715b90 --- /dev/null +++ b/src/services/blob.js @@ -0,0 +1,28 @@ +const becca = require('../becca/becca'); +const NotFoundError = require("../errors/not_found_error"); + +function getBlobPojo(entityName, entityId, opts = {}) { + opts.full = !!opts.full; + + const entity = becca.getEntity(entityName, entityId); + + if (!entity) { + throw new NotFoundError(`Entity ${entityName} '${entityId}' was not found.`); + } + + const blob = becca.getBlob(entity.blobId); + + const pojo = blob.getPojo(); + + if (!entity.hasStringContent()) { + pojo.content = null; + } else if (!opts.full && pojo.content.length > 10000) { + pojo.content = `${pojo.content.substr(0, 10000)}\r\n\r\n... and ${pojo.content.length - 10000} more characters.`; + } + + return pojo; +} + +module.exports = { + getBlobPojo +}; \ No newline at end of file diff --git a/src/services/export/opml.js b/src/services/export/opml.js index 4fce57515..346a6b081 100644 --- a/src/services/export/opml.js +++ b/src/services/export/opml.js @@ -24,13 +24,13 @@ function exportToOpml(taskContext, branch, version, res) { if (opmlVersion === 1) { const preparedTitle = escapeXmlAttribute(title); - const preparedContent = note.isStringNote() ? prepareText(note.getContent()) : ''; + const preparedContent = note.hasStringContent() ? prepareText(note.getContent()) : ''; res.write(`\n`); } else if (opmlVersion === 2) { const preparedTitle = escapeXmlAttribute(title); - const preparedContent = note.isStringNote() ? escapeXmlAttribute(note.getContent()) : ''; + const preparedContent = note.hasStringContent() ? escapeXmlAttribute(note.getContent()) : ''; res.write(`\n`); } diff --git a/src/services/handlers.js b/src/services/handlers.js index 6ae306cfc..766cf2f6c 100644 --- a/src/services/handlers.js +++ b/src/services/handlers.js @@ -90,7 +90,7 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) => if (["text", "code"].includes(note.type) // if the note has already content we're not going to overwrite it with template's one && (!content || content.trim().length === 0) - && templateNote.isStringNote()) { + && templateNote.hasStringContent()) { const templateNoteContent = templateNote.getContent(); diff --git a/src/services/notes.js b/src/services/notes.js index d0e1b2ac6..237a7bbf3 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -21,7 +21,6 @@ const htmlSanitizer = require("./html_sanitizer"); const ValidationError = require("../errors/validation_error"); const noteTypesService = require("./note_types"); const fs = require("fs"); -const BAttachment = require("../becca/entities/battachment"); /** @param {BNote} parentNote */ function getNewNotePosition(parentNote) { diff --git a/src/share/shaca/entities/snote.js b/src/share/shaca/entities/snote.js index 4a80e0157..f28260deb 100644 --- a/src/share/shaca/entities/snote.js +++ b/src/share/shaca/entities/snote.js @@ -107,7 +107,7 @@ class SNote extends AbstractShacaEntity { let content = row.content; - if (this.isStringNote()) { + if (this.hasStringContent()) { return content === null ? "" : content.toString("UTF-8"); @@ -118,7 +118,7 @@ class SNote extends AbstractShacaEntity { } /** @returns {boolean} true if the note has string content (not binary) */ - isStringNote() { + hasStringContent() { return utils.isStringNote(this.type, this.mime); }