From 5be61e6142fa08587eb37e0192f4fd69e2edab3d Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 2 Oct 2023 15:24:40 +0200 Subject: [PATCH 1/3] saving / viewing canvas revisions --- src/becca/entities/bnote.js | 14 ++--- src/becca/entities/brevision.js | 62 +++++++++++++++++++++ src/public/app/widgets/dialogs/revisions.js | 23 ++------ src/routes/api/image.js | 24 ++++++-- src/routes/routes.js | 4 +- 5 files changed, 94 insertions(+), 33 deletions(-) diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js index 1f05b4001..6e02bf01c 100644 --- a/src/becca/entities/bnote.js +++ b/src/becca/entities/bnote.js @@ -1607,16 +1607,12 @@ class BNote extends AbstractBeccaEntity { revision.save(); // to generate revisionId, which is then used to save attachments - if (this.type === 'text') { - for (const noteAttachment of this.getAttachments()) { - if (noteAttachment.utcDateScheduledForErasureSince) { - continue; - } - - const revisionAttachment = noteAttachment.copy(); - revisionAttachment.ownerId = revision.revisionId; - revisionAttachment.setContent(noteAttachment.getContent(), {forceSave: true}); + for (const noteAttachment of this.getAttachments()) { + const revisionAttachment = noteAttachment.copy(); + revisionAttachment.ownerId = revision.revisionId; + revisionAttachment.setContent(noteAttachment.getContent(), {forceSave: true}); + if (this.type === 'text') { // content is rewritten to point to the revision attachments noteContent = noteContent.replaceAll(`attachments/${noteAttachment.attachmentId}`, `attachments/${revisionAttachment.attachmentId}`); diff --git a/src/becca/entities/brevision.js b/src/becca/entities/brevision.js index 2e0f1f33e..09a78db64 100644 --- a/src/becca/entities/brevision.js +++ b/src/becca/entities/brevision.js @@ -86,6 +86,29 @@ class BRevision extends AbstractBeccaEntity { return this._getContent(); } + /** + * @returns {*} + * @throws Error in case of invalid JSON */ + getJsonContent() { + const content = this.getContent(); + + if (!content || !content.trim()) { + return null; + } + + return JSON.parse(content); + } + + /** @returns {*|null} valid object or null if the content cannot be parsed as JSON */ + getJsonContentSafely() { + try { + return this.getJsonContent(); + } + catch (e) { + return null; + } + } + /** * @param content * @param {object} [opts] @@ -105,6 +128,45 @@ class BRevision extends AbstractBeccaEntity { .map(row => new BAttachment(row)); } + /** @returns {BAttachment|null} */ + getAttachmentById(attachmentId, opts = {}) { + opts.includeContentLength = !!opts.includeContentLength; + + const query = opts.includeContentLength + ? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength + FROM attachments + JOIN blobs USING (blobId) + WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0` + : `SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`; + + return sql.getRows(query, [this.revisionId, attachmentId]) + .map(row => new BAttachment(row))[0]; + } + + /** @returns {BAttachment[]} */ + getAttachmentsByRole(role) { + return sql.getRows(` + SELECT attachments.* + FROM attachments + WHERE ownerId = ? + AND role = ? + AND isDeleted = 0 + ORDER BY position`, [this.revisionId, role]) + .map(row => new BAttachment(row)); + } + + /** @returns {BAttachment} */ + getAttachmentByTitle(title) { + return sql.getRows(` + SELECT attachments.* + FROM attachments + WHERE ownerId = ? + AND title = ? + AND isDeleted = 0 + ORDER BY position`, [this.revisionId, title]) + .map(row => new BAttachment(row))[0]; + } + beforeSaving() { super.beforeSaving(); diff --git a/src/public/app/widgets/dialogs/revisions.js b/src/public/app/widgets/dialogs/revisions.js index 1fa3eedc0..d5a56328c 100644 --- a/src/public/app/widgets/dialogs/revisions.js +++ b/src/public/app/widgets/dialogs/revisions.js @@ -274,26 +274,11 @@ export default class RevisionsDialog extends BasicWidget { this.$content.html($table); } else if (revisionItem.type === 'canvas') { - /** - * FIXME: We load a font called Virgil.wof2, which originates from excalidraw.com - * REMOVE external dependency!!!! This is defined in the svg in defs.style - */ - const content = fullRevision.content; + const sanitizedTitle = revisionItem.title.replace(/[^a-z0-9-.]/gi, ""); - try { - const data = JSON.parse(content) - const svg = data.svg || "no svg present." - - /** - * maxWidth: 100% use full width of container but do not enlarge! - * height:auto to ensure that height scales with width - */ - const $svgHtml = $(svg).css({maxWidth: "100%", height: "auto"}); - this.$content.html($('
').append($svgHtml)); - } catch (err) { - console.error("error parsing fullRevision.content as JSON", fullRevision.content, err); - this.$content.html($("
").text("Error parsing content. Please check console.error() for more details.")); - } + this.$content.html($("") + .attr("src", `api/revisions/${revisionItem.revisionId}/image/${sanitizedTitle}?${Math.random()}`) + .css("max-width", "100%")); } else { this.$content.text("Preview isn't available for this note type."); } diff --git a/src/routes/api/image.js b/src/routes/api/image.js index 902ad9e55..26c30db02 100644 --- a/src/routes/api/image.js +++ b/src/routes/api/image.js @@ -5,14 +5,27 @@ const becca = require('../../becca/becca'); const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR; const fs = require('fs'); -function returnImage(req, res) { +function returnImageFromNote(req, res) { const image = becca.getNote(req.params.noteId); + return returnImageInt(image, res); +} + +function returnImageFromRevision(req, res) { + const image = becca.getRevision(req.params.revisionId); + + return returnImageInt(image, res); +} + +/** + * @param {BNote|BRevision} image + * @param res + */ +function returnImageInt(image, res) { if (!image) { res.set('Content-Type', 'image/png'); return res.send(fs.readFileSync(`${RESOURCE_DIR}/db/image-deleted.png`)); - } - else if (!["image", "canvas"].includes(image.type)){ + } else if (!["image", "canvas"].includes(image.type)) { return res.sendStatus(400); } @@ -24,6 +37,8 @@ function returnImage(req, res) { let svgString = '' const attachment = image.getAttachmentByTitle('canvas-export.svg'); + console.log(attachment); + if (attachment) { svgString = attachment.getContent(); } else { @@ -84,7 +99,8 @@ function updateImage(req) { } module.exports = { - returnImage, + returnImageFromNote, + returnImageFromRevision, returnAttachedImage, updateImage }; diff --git a/src/routes/routes.js b/src/routes/routes.js index 50ef93634..97a2f8de8 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -181,6 +181,8 @@ function register(app) { apiRoute(GET, '/api/revisions/:revisionId/blob', revisionsApiRoute.getRevisionBlob); apiRoute(DEL, '/api/revisions/:revisionId', revisionsApiRoute.eraseRevision); apiRoute(PST, '/api/revisions/:revisionId/restore', revisionsApiRoute.restoreRevision); + route(GET, '/api/revisions/:revisionId/image/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImageFromRevision); + route(GET, '/api/revisions/:revisionId/download', [auth.checkApiAuthOrElectron], revisionsApiRoute.downloadRevision); @@ -200,7 +202,7 @@ function register(app) { apiRoute(GET, '/api/attribute-values/:attributeName', attributesRoute.getValuesForAttribute); // :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename - route(GET, '/api/images/:noteId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImage); + route(GET, '/api/images/:noteId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImageFromNote); route(PUT, '/api/images/:noteId', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], imageRoute.updateImage, apiResultHandler); apiRoute(GET, '/api/options', optionsApiRoute.getOptions); From 3944235592cb25f1612402d883cd69ea8380fc4a Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 2 Oct 2023 15:25:12 +0200 Subject: [PATCH 2/3] -console.log --- src/routes/api/image.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/routes/api/image.js b/src/routes/api/image.js index 26c30db02..8f44eb142 100644 --- a/src/routes/api/image.js +++ b/src/routes/api/image.js @@ -37,8 +37,6 @@ function returnImageInt(image, res) { let svgString = '' const attachment = image.getAttachmentByTitle('canvas-export.svg'); - console.log(attachment); - if (attachment) { svgString = attachment.getContent(); } else { From 91eb3c45d534236cc0b4103914cdb710db17a936 Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 2 Oct 2023 15:29:45 +0200 Subject: [PATCH 3/3] added "save revision" to note actions menu --- src/public/app/widgets/buttons/note_actions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/public/app/widgets/buttons/note_actions.js b/src/public/app/widgets/buttons/note_actions.js index 4d48cff41..6e2ebb0cc 100644 --- a/src/public/app/widgets/buttons/note_actions.js +++ b/src/public/app/widgets/buttons/note_actions.js @@ -45,6 +45,7 @@ const TPL = ` Export note Delete note Print note + Save revision
`;