From 2b84f1be005c6f8074d1ec484455a9d6b716f9fa Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 16 Mar 2023 15:19:26 +0100 Subject: [PATCH] unify .setContent() .getContent() handling across notes, revisions, attachments --- db/migrations/0217__attachments.sql | 1 + src/becca/entities/abstract_becca_entity.js | 108 +++++++++++++++ src/becca/entities/battachment.js | 69 ++-------- src/becca/entities/bnote.js | 140 ++++---------------- src/becca/entities/bnote_revision.js | 70 +--------- src/services/attachments.js | 4 +- src/services/consistency_checks.js | 8 +- 7 files changed, 151 insertions(+), 249 deletions(-) diff --git a/db/migrations/0217__attachments.sql b/db/migrations/0217__attachments.sql index 9e2f821da..2958aa2d2 100644 --- a/db/migrations/0217__attachments.sql +++ b/db/migrations/0217__attachments.sql @@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS "attachments" title TEXT not null, isProtected INT not null DEFAULT 0, blobId TEXT not null, + utcDateScheduledForDeletionSince TEXT DEFAULT NULL, utcDateModified TEXT not null, isDeleted INT not null, deleteId TEXT DEFAULT NULL); diff --git a/src/becca/entities/abstract_becca_entity.js b/src/becca/entities/abstract_becca_entity.js index c1c894de7..918e31d74 100644 --- a/src/becca/entities/abstract_becca_entity.js +++ b/src/becca/entities/abstract_becca_entity.js @@ -7,6 +7,7 @@ const eventService = require("../../services/events"); const dateUtils = require("../../services/date_utils"); const cls = require("../../services/cls"); const log = require("../../services/log"); +const protectedSessionService = require("../../services/protected_session.js"); let becca = null; @@ -118,6 +119,113 @@ class AbstractBeccaEntity { return this; } + /** @protected */ + _isHot() { + return false; + } + + /** @protected */ + _setContent(content) { + if (content === null || content === undefined) { + throw new Error(`Cannot set null content to ${this.constructor.primaryKeyName} '${this[this.constructor.primaryKeyName]}'`); + } + + if (this.isStringNote()) { + content = content.toString(); + } + else { + content = Buffer.isBuffer(content) ? content : Buffer.from(content); + } + + if (this.isProtected) { + if (protectedSessionService.isProtectedSessionAvailable()) { + content = protectedSessionService.encrypt(content); + } + else { + throw new Error(`Cannot update content of blob since we're out of protected session.`); + } + } + + sql.transactional(() => { + let newBlobId = this._saveBlob(content); + + if (newBlobId !== this.blobId) { + this.blobId = newBlobId; + this.save(); + } + }); + } + + /** @protected */ + _saveBlob(content) { + let newBlobId; + let blobNeedsInsert; + + if (this._isHot()) { + newBlobId = this.blobId || utils.randomBlobId(); + blobNeedsInsert = true; + } else { + newBlobId = utils.hashedBlobId(content); + blobNeedsInsert = !sql.getValue('SELECT 1 FROM blobs WHERE blobId = ?', [newBlobId]); + } + + if (blobNeedsInsert) { + const pojo = { + blobId: newBlobId, + content: content, + dateModified: dateUtils.localNowDateTime(), + utcDateModified: dateUtils.utcNowDateTime() + }; + + sql.upsert("blobs", "blobId", pojo); + + const hash = utils.hash(`${newBlobId}|${pojo.content.toString()}`); + + entityChangesService.addEntityChange({ + entityName: 'blobs', + entityId: newBlobId, + hash: hash, + isErased: false, + utcDateChanged: pojo.utcDateModified, + isSynced: true + }); + + eventService.emit(eventService.ENTITY_CHANGED, { + entityName: 'blobs', + entity: this + }); + } + + return newBlobId; + } + + /** @protected */ + _getContent() { + const row = sql.getRow(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]); + + if (!row) { + throw new Error(`Cannot find content for ${this.constructor.primaryKeyName} '${this[this.constructor.primaryKeyName]}', blobId '${this.blobId}'`); + } + + let content = row.content; + + if (this.isProtected) { + if (protectedSessionService.isProtectedSessionAvailable()) { + content = content === null ? null : protectedSessionService.decrypt(content); + } else { + content = ""; + } + } + + if (this.isStringNote()) { + return content === null + ? "" + : content.toString("UTF-8"); + } else { + return content; + } + } + /** * Mark the entity as (soft) deleted. It will be completely erased later. * diff --git a/src/becca/entities/battachment.js b/src/becca/entities/battachment.js index bcdde8302..92f190c55 100644 --- a/src/becca/entities/battachment.js +++ b/src/becca/entities/battachment.js @@ -17,7 +17,8 @@ const AbstractBeccaEntity = require("./abstract_becca_entity"); class BAttachment extends AbstractBeccaEntity { static get entityName() { return "attachments"; } static get primaryKeyName() { return "attachmentId"; } - static get hashedProperties() { return ["attachmentId", "parentId", "role", "mime", "title", "utcDateModified"]; } + static get hashedProperties() { return ["attachmentId", "parentId", "role", "mime", "title", "blobId", + "utcDateScheduledForDeletionSince", "utcDateModified"]; } constructor(row) { super(); @@ -32,7 +33,7 @@ class BAttachment extends AbstractBeccaEntity { throw new Error("'title' must be given to initialize a Attachment entity"); } - /** @type {string} needs to be set at the initialization time since it's used in the .setContent() */ + /** @type {string} */ this.attachmentId = row.attachmentId || `${this.noteId}_${this.name}`; // FIXME /** @type {string} either noteId or noteRevisionId to which this attachment belongs */ this.parentId = row.parentId; @@ -45,6 +46,8 @@ class BAttachment extends AbstractBeccaEntity { /** @type {boolean} */ this.isProtected = !!row.isProtected; /** @type {string} */ + this.utcDateScheduledForDeletionSince = row.utcDateScheduledForDeletionSince; + /** @type {string} */ this.utcDateModified = row.utcDateModified; } @@ -58,68 +61,12 @@ class BAttachment extends AbstractBeccaEntity { } /** @returns {*} */ - getContent(silentNotFoundError = false) { - const res = sql.getRow(`SELECT content FROM attachment_contents WHERE attachmentId = ?`, [this.attachmentId]); - - if (!res) { - if (silentNotFoundError) { - return undefined; - } - else { - throw new Error(`Cannot find note attachment content for attachmentId=${this.attachmentId}`); - } - } - - 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; - } + getContent() { + return this._getContent(); } setContent(content) { - sql.transactional(() => { - this.save(); // also explicitly save attachment to update contentCheckSum - - const pojo = { - attachmentId: this.attachmentId, - 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 attachmentId=${this.attachmentId} since we're out of protected session.`); - } - } - - sql.upsert("attachment_contents", "attachmentId", pojo); - - entityChangesService.addEntityChange({ - entityName: 'attachment_contents', - entityId: this.attachmentId, - hash: this.contentCheckSum, // FIXME - isErased: false, - utcDateChanged: pojo.utcDateModified, - isSynced: true - }); - }); + this._setContent(content); } calculateCheckSum(content) { diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js index f491235fa..2b310b5a8 100644 --- a/src/becca/entities/bnote.js +++ b/src/becca/entities/bnote.js @@ -208,37 +208,8 @@ class BNote extends AbstractBeccaEntity { */ /** @returns {*} */ - getContent(silentNotFoundError = false) { - const row = sql.getRow(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]); - - if (!row) { - if (silentNotFoundError) { - return undefined; - } - else { - throw new Error(`Cannot find note content for noteId '${this.noteId}', blobId '${this.blobId}'.`); - } - } - - let content = row.content; - - if (this.isProtected) { - if (protectedSessionService.isProtectedSessionAvailable()) { - content = content === null ? null : protectedSessionService.decrypt(content); - } - else { - content = ""; - } - } - - if (this.isStringNote()) { - return content === null - ? "" - : content.toString("UTF-8"); - } - else { - return content; - } + getContent() { + return this._getContent(); } /** @returns {{contentLength, dateModified, utcDateModified}} */ @@ -252,6 +223,29 @@ class BNote extends AbstractBeccaEntity { WHERE blobId = ?`, [this.blobId]); } + /** @returns {*} */ + getJsonContent() { + const content = this.getContent(); + + if (!content || !content.trim()) { + return null; + } + + return JSON.parse(content); + } + + _isHot() { + return ['text', 'code', 'relationMap', 'canvas', 'mermaid'].includes(this.type); + } + + setContent(content) { + this._setContent(content) + } + + setJsonContent(content) { + this.setContent(JSON.stringify(content, null, '\t')); + } + get dateCreatedObj() { return this.dateCreated === null ? null : dayjs(this.dateCreated); } @@ -268,90 +262,6 @@ class BNote extends AbstractBeccaEntity { return this.utcDateModified === null ? null : dayjs.utc(this.utcDateModified); } - /** @returns {*} */ - getJsonContent() { - const content = this.getContent(); - - if (!content || !content.trim()) { - return null; - } - - return JSON.parse(content); - } - - isHot() { - return ['text', 'code', 'relationMap', 'canvas', 'mermaid'].includes(this.type); - } - - setContent(content, ignoreMissingProtectedSession = false) { - if (content === null || content === undefined) { - throw new Error(`Cannot set null content to note '${this.noteId}'`); - } - - if (this.isStringNote()) { - content = content.toString(); - } - else { - content = Buffer.isBuffer(content) ? content : Buffer.from(content); - } - - if (this.isProtected) { - if (protectedSessionService.isProtectedSessionAvailable()) { - content = protectedSessionService.encrypt(content); - } - else if (!ignoreMissingProtectedSession) { - throw new Error(`Cannot update content of noteId '${this.noteId}' since we're out of protected session.`); - } - } - - let newBlobId; - let blobNeedsInsert; - - if (this.isHot()) { - newBlobId = this.blobId || utils.randomBlobId(); - blobNeedsInsert = true; - } else { - newBlobId = utils.hashedBlobId(content); - blobNeedsInsert = !sql.getValue('SELECT 1 FROM blobs WHERE blobId = ?', [newBlobId]); - } - - if (blobNeedsInsert) { - const pojo = { - blobId: this.blobId, - content: content, - dateModified: dateUtils.localNowDateTime(), - utcDateModified: dateUtils.utcNowDateTime() - }; - - sql.upsert("blobs", "blobId", pojo); - - const hash = utils.hash(`${this.blobId}|${pojo.content.toString()}`); - - entityChangesService.addEntityChange({ - entityName: 'blobs', - entityId: this.blobId, - hash: hash, - isErased: false, - utcDateChanged: pojo.utcDateModified, - isSynced: true - }); - - eventService.emit(eventService.ENTITY_CHANGED, { - entityName: 'blobs', - entity: this - }); - } - - if (newBlobId !== this.blobId) { - this.blobId = newBlobId; - this.save(); - } - } - - setJsonContent(content) { - this.setContent(JSON.stringify(content, null, '\t')); - } - /** @returns {boolean} true if this note is the root of the note tree. Root note has "root" noteId */ isRoot() { return this.noteId === 'root'; diff --git a/src/becca/entities/bnote_revision.js b/src/becca/entities/bnote_revision.js index d0fe74cdc..6e8fedfc4 100644 --- a/src/becca/entities/bnote_revision.js +++ b/src/becca/entities/bnote_revision.js @@ -75,76 +75,12 @@ class BNoteRevision extends AbstractBeccaEntity { */ /** @returns {*} */ - getContent(silentNotFoundError = false) { - const res = sql.getRow(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]); - - if (!res) { - if (silentNotFoundError) { - return undefined; - } - else { - throw new Error(`Cannot find note revision content for noteRevisionId '${this.noteRevisionId}', blobId '${this.blobId}'`); - } - } - - 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; - } + getContent() { + return this._getContent(); } setContent(content) { - if (this.isProtected) { - if (protectedSessionService.isProtectedSessionAvailable()) { - content = protectedSessionService.encrypt(content); - } - else { - throw new Error(`Cannot update content of noteRevisionId '${this.noteRevisionId}' since we're out of protected session.`); - } - } - - this.blobId = utils.hashedBlobId(content); - - const blobAlreadyExists = !!sql.getValue('SELECT 1 FROM blobs WHERE blobId = ?', [this.blobId]); - - if (!blobAlreadyExists) { - const pojo = { - blobId: this.blobId, - content: content, - dateModified: dateUtils.localNowDate(), - utcDateModified: dateUtils.utcNowDateTime() - }; - - sql.insert("blobs", pojo); - - const hash = utils.hash(`${this.noteRevisionId}|${pojo.content.toString()}`); - - entityChangesService.addEntityChange({ - entityName: 'blobs', - entityId: this.blobId, - hash: hash, - isErased: false, - utcDateChanged: this.getUtcDateChanged(), - isSynced: true - }); - } - - this.save(); // saving this.blobId + this._setContent(content); } beforeSaving() { diff --git a/src/services/attachments.js b/src/services/attachments.js index b6b3d3ed5..06b2ca9e4 100644 --- a/src/services/attachments.js +++ b/src/services/attachments.js @@ -8,7 +8,7 @@ function protectAttachments(note) { for (const attachment of note.getAttachments()) { if (note.isProtected !== attachment.isProtected) { if (!protectedSession.isProtectedSessionAvailable()) { - log.error("Protected session is not available to fix note attachments."); + log.error("Protected session is not available to fix attachments."); return; } @@ -24,7 +24,7 @@ function protectAttachments(note) { attachment.save(); } catch (e) { - log.error(`Could not un/protect note attachment ID = ${attachment.attachmentId}`); + log.error(`Could not un/protect attachment ID = ${attachment.attachmentId}`); throw e; } diff --git a/src/services/consistency_checks.js b/src/services/consistency_checks.js index 5bc23ce6d..fb51265a5 100644 --- a/src/services/consistency_checks.js +++ b/src/services/consistency_checks.js @@ -231,9 +231,9 @@ class ConsistencyChecks { this.reloadNeeded = false; - logFix(`Note attachment '${attachmentId}' has been deleted since it references missing note/revision '${parentId}'`); + logFix(`Attachment '${attachmentId}' has been deleted since it references missing note/revision '${parentId}'`); } else { - logError(`Note attachment '${attachmentId}' references missing note/revision '${parentId}'`); + logError(`Attachment '${attachmentId}' references missing note/revision '${parentId}'`); } }); } @@ -358,9 +358,9 @@ class ConsistencyChecks { this.reloadNeeded = false; - logFix(`Note attachment '${attachmentId}' has been deleted since associated note '${noteId}' is deleted.`); + logFix(`Attachment '${attachmentId}' has been deleted since associated note '${noteId}' is deleted.`); } else { - logError(`Note attachment '${attachmentId}' is not deleted even though associated note '${noteId}' is deleted.`) + logError(`Attachment '${attachmentId}' is not deleted even though associated note '${noteId}' is deleted.`) } }); }