From 44bcfd47c0bab06c8ecd442c9118a643c41ef381 Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 5 Jun 2023 23:05:05 +0200 Subject: [PATCH] share support for attachment images --- src/becca/entities/abstract_becca_entity.js | 2 - src/share/routes.js | 41 +++++++++- .../shaca/entities/abstract_shaca_entity.js | 1 + src/share/shaca/entities/sattachment.js | 77 +++++++++++++++++++ src/share/shaca/entities/sattribute.js | 2 +- src/share/shaca/entities/snote.js | 14 +++- src/share/shaca/shaca.js | 7 ++ src/share/shaca/shaca_loader.js | 15 +++- 8 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 src/share/shaca/entities/sattachment.js diff --git a/src/becca/entities/abstract_becca_entity.js b/src/becca/entities/abstract_becca_entity.js index 46582b315..d812443e1 100644 --- a/src/becca/entities/abstract_becca_entity.js +++ b/src/becca/entities/abstract_becca_entity.js @@ -93,8 +93,6 @@ class AbstractBeccaEntity { const pojo = this.getPojoToSave(); - console.log(pojo); - sql.transactional(() => { sql.upsert(entityName, primaryKeyName, pojo); diff --git a/src/share/routes.js b/src/share/routes.js index 2d9aa5048..b0cb1b7cf 100644 --- a/src/share/routes.js +++ b/src/share/routes.js @@ -37,13 +37,30 @@ function requestCredentials(res) { .sendStatus(401); } +/** @returns {SAttachment|boolean} */ +function checkAttachmentAccess(attachmentId, req, res) { + const attachment = shaca.getAttachment(attachmentId); + + if (!attachment) { + res.status(404) + .json({ message: `Attachment '${attachmentId}' not found.` }); + + return false; + } + + const note = checkNoteAccess(attachment.parentId, req, res); + + // truthy note means user has access, and we can return the attachment + return note ? attachment : false; +} + /** @returns {SNote|boolean} */ function checkNoteAccess(noteId, req, res) { const note = shaca.getNote(noteId); if (!note) { res.status(404) - .json({ message: `Note '${noteId}' not found` }); + .json({ message: `Note '${noteId}' not found.` }); return false; } @@ -151,7 +168,7 @@ function register(router) { addNoIndexHeader(note, res); - res.json(note.getPojoWithAttributes()); + res.json(note.getPojo()); }); router.get('/share/api/notes/:noteId/download', (req, res, next) => { @@ -216,6 +233,26 @@ function register(router) { } }); + // :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename + router.get('/share/api/attachments/:attachmentId/image/:filename', (req, res, next) => { + shacaLoader.ensureLoad(); + + let attachment; + + if (!(attachment = checkAttachmentAccess(req.params.attachmentId, req, res))) { + return; + } + + if (attachment.role === "image") { + res.set('Content-Type', attachment.mime); + addNoIndexHeader(attachment.note, res); + res.send(attachment.getContent()); + } else { + return res.status(400) + .json({ message: "Requested attachment is not a shareable image" }); + } + }); + // used for PDF viewing router.get('/share/api/notes/:noteId/view', (req, res, next) => { shacaLoader.ensureLoad(); diff --git a/src/share/shaca/entities/abstract_shaca_entity.js b/src/share/shaca/entities/abstract_shaca_entity.js index 8f3291091..a6753e9b7 100644 --- a/src/share/shaca/entities/abstract_shaca_entity.js +++ b/src/share/shaca/entities/abstract_shaca_entity.js @@ -1,6 +1,7 @@ let shaca; class AbstractShacaEntity { + /** @return {Shaca} */ get shaca() { if (!shaca) { shaca = require("../shaca"); diff --git a/src/share/shaca/entities/sattachment.js b/src/share/shaca/entities/sattachment.js new file mode 100644 index 000000000..d228bb79c --- /dev/null +++ b/src/share/shaca/entities/sattachment.js @@ -0,0 +1,77 @@ +"use strict"; + +const sql = require('../../sql'); +const utils = require('../../../services/utils'); +const AbstractShacaEntity = require('./abstract_shaca_entity'); + +class SAttachment extends AbstractShacaEntity { + constructor([attachmentId, parentId, role, mime, title, blobId, utcDateModified]) { + super(); + + /** @param {string} */ + this.attachmentId = attachmentId; + /** @param {string} */ + this.parentId = parentId; + /** @param {string} */ + this.title = title; + /** @param {string} */ + this.role = role; + /** @param {string} */ + this.mime = mime; + /** @param {string} */ + this.blobId = blobId; + /** @param {string} */ + this.utcDateModified = utcDateModified; // used for caching of images + + this.shaca.attachments[this.attachmentId] = this; + this.shaca.notes[this.parentId].attachments.push(this); + } + + /** @returns {SNote} */ + get note() { + return this.shaca.notes[this.parentId]; + } + + 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 blob for attachment '${this.attachmentId}', blob '${this.blobId}'`); + } + } + + let content = row.content; + + if (this.hasStringContent()) { + return content === null + ? "" + : content.toString("utf-8"); + } + else { + return content; + } + } + + /** @returns {boolean} true if the attachment has string content (not binary) */ + hasStringContent() { + return utils.isStringNote(null, this.mime); + } + + getPojo() { + return { + attachmentId: this.attachmentId, + role: this.role, + mime: this.mime, + title: this.title, + position: this.position, + blobId: this.blobId, + utcDateModified: this.utcDateModified + }; + } +} + +module.exports = SAttachment; diff --git a/src/share/shaca/entities/sattribute.js b/src/share/shaca/entities/sattribute.js index 026a8eb01..083e6ac4d 100644 --- a/src/share/shaca/entities/sattribute.js +++ b/src/share/shaca/entities/sattribute.js @@ -69,7 +69,7 @@ class SAttribute extends AbstractShacaEntity { return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name); } - /** @returns {SNote|null} */ + /** @returns {SNote} */ get note() { return this.shaca.notes[this.noteId]; } diff --git a/src/share/shaca/entities/snote.js b/src/share/shaca/entities/snote.js index 3d6cc4d96..2f4bc2fac 100644 --- a/src/share/shaca/entities/snote.js +++ b/src/share/shaca/entities/snote.js @@ -47,6 +47,9 @@ class SNote extends AbstractShacaEntity { /** @param {SAttribute[]} */ this.targetRelations = []; + /** @param {SAttachment[]} */ + this.attachments = []; + this.shaca.notes[this.noteId] = this; } @@ -101,7 +104,7 @@ class SNote extends AbstractShacaEntity { return undefined; } else { - throw new Error(`Cannot find note content for noteId '${this.noteId}', blobId '${this.blobId}'`); + throw new Error(`Cannot find note content for note '${this.noteId}', blob '${this.blobId}'`); } } @@ -442,6 +445,11 @@ class SNote extends AbstractShacaEntity { return this.targetRelations; } + /** @returns {SAttachment[]} */ + getAttachments() { + return this.attachments; + } + /** @returns {string} */ get shareId() { if (this.hasOwnedLabel('shareRoot')) { @@ -457,7 +465,7 @@ class SNote extends AbstractShacaEntity { return escape(this.title); } - getPojoWithAttributes() { + getPojo() { return { noteId: this.noteId, title: this.title, @@ -469,6 +477,8 @@ class SNote extends AbstractShacaEntity { // individual relations might be whitelisted based on needs #3434 .filter(attr => attr.type === 'label') .map(attr => attr.getPojo()), + attachments: this.getAttachments() + .map(attachment => attachment.getPojo()), parentNoteIds: this.parents.map(parentNote => parentNote.noteId), childNoteIds: this.children.map(child => child.noteId) }; diff --git a/src/share/shaca/shaca.js b/src/share/shaca/shaca.js index 19c3f991f..694f76b69 100644 --- a/src/share/shaca/shaca.js +++ b/src/share/shaca/shaca.js @@ -14,6 +14,8 @@ class Shaca { this.childParentToBranch = {}; /** @type {Object.} */ this.attributes = {}; + /** @type {Object.} */ + this.attachments = {}; /** @type {Object.} */ this.aliasToNote = {}; @@ -72,6 +74,11 @@ class Shaca { return this.attributes[attributeId]; } + /** @returns {SAttachment|null} */ + getAttachment(attachmentId) { + return this.attachments[attachmentId]; + } + getEntity(entityName, entityId) { if (!entityName || !entityId) { return null; diff --git a/src/share/shaca/shaca_loader.js b/src/share/shaca/shaca_loader.js index 7ecaaf617..361db0cc0 100644 --- a/src/share/shaca/shaca_loader.js +++ b/src/share/shaca/shaca_loader.js @@ -6,6 +6,7 @@ const log = require('../../services/log'); const SNote = require('./entities/snote'); const SBranch = require('./entities/sbranch'); const SAttribute = require('./entities/sattribute'); +const SAttachment = require("./entities/sattachment"); const shareRoot = require('../share_root'); const eventService = require("../../services/events"); @@ -65,9 +66,21 @@ function load() { new SAttribute(row); } + const rawAttachmentRows = sql.getRawRows(` + SELECT attachmentId, parentId, role, mime, title, blobId, utcDateModified + FROM attachments + WHERE isDeleted = 0 + AND parentId IN (${noteIdStr})`); + + rawAttachmentRows.sort((a, b) => a.position < b.position ? -1 : 1); + + for (const row of rawAttachmentRows) { + new SAttachment(row); + } + shaca.loaded = true; - log.info(`Shaca loaded ${rawNoteRows.length} notes, ${rawBranchRows.length} branches, ${rawAttributeRows.length} attributes took ${Date.now() - start}ms`); + log.info(`Shaca loaded ${rawNoteRows.length} notes, ${rawBranchRows.length} branches, ${rawAttachmentRows.length} attributes took ${Date.now() - start}ms`); } function ensureLoad() {