share support for attachment images

This commit is contained in:
zadam 2023-06-05 23:05:05 +02:00
parent 69c7eb14aa
commit 44bcfd47c0
8 changed files with 151 additions and 8 deletions

View File

@ -93,8 +93,6 @@ class AbstractBeccaEntity {
const pojo = this.getPojoToSave(); const pojo = this.getPojoToSave();
console.log(pojo);
sql.transactional(() => { sql.transactional(() => {
sql.upsert(entityName, primaryKeyName, pojo); sql.upsert(entityName, primaryKeyName, pojo);

View File

@ -37,13 +37,30 @@ function requestCredentials(res) {
.sendStatus(401); .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} */ /** @returns {SNote|boolean} */
function checkNoteAccess(noteId, req, res) { function checkNoteAccess(noteId, req, res) {
const note = shaca.getNote(noteId); const note = shaca.getNote(noteId);
if (!note) { if (!note) {
res.status(404) res.status(404)
.json({ message: `Note '${noteId}' not found` }); .json({ message: `Note '${noteId}' not found.` });
return false; return false;
} }
@ -151,7 +168,7 @@ function register(router) {
addNoIndexHeader(note, res); addNoIndexHeader(note, res);
res.json(note.getPojoWithAttributes()); res.json(note.getPojo());
}); });
router.get('/share/api/notes/:noteId/download', (req, res, next) => { 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 // used for PDF viewing
router.get('/share/api/notes/:noteId/view', (req, res, next) => { router.get('/share/api/notes/:noteId/view', (req, res, next) => {
shacaLoader.ensureLoad(); shacaLoader.ensureLoad();

View File

@ -1,6 +1,7 @@
let shaca; let shaca;
class AbstractShacaEntity { class AbstractShacaEntity {
/** @return {Shaca} */
get shaca() { get shaca() {
if (!shaca) { if (!shaca) {
shaca = require("../shaca"); shaca = require("../shaca");

View File

@ -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;

View File

@ -69,7 +69,7 @@ class SAttribute extends AbstractShacaEntity {
return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name); return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name);
} }
/** @returns {SNote|null} */ /** @returns {SNote} */
get note() { get note() {
return this.shaca.notes[this.noteId]; return this.shaca.notes[this.noteId];
} }

View File

@ -47,6 +47,9 @@ class SNote extends AbstractShacaEntity {
/** @param {SAttribute[]} */ /** @param {SAttribute[]} */
this.targetRelations = []; this.targetRelations = [];
/** @param {SAttachment[]} */
this.attachments = [];
this.shaca.notes[this.noteId] = this; this.shaca.notes[this.noteId] = this;
} }
@ -101,7 +104,7 @@ class SNote extends AbstractShacaEntity {
return undefined; return undefined;
} }
else { 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; return this.targetRelations;
} }
/** @returns {SAttachment[]} */
getAttachments() {
return this.attachments;
}
/** @returns {string} */ /** @returns {string} */
get shareId() { get shareId() {
if (this.hasOwnedLabel('shareRoot')) { if (this.hasOwnedLabel('shareRoot')) {
@ -457,7 +465,7 @@ class SNote extends AbstractShacaEntity {
return escape(this.title); return escape(this.title);
} }
getPojoWithAttributes() { getPojo() {
return { return {
noteId: this.noteId, noteId: this.noteId,
title: this.title, title: this.title,
@ -469,6 +477,8 @@ class SNote extends AbstractShacaEntity {
// individual relations might be whitelisted based on needs #3434 // individual relations might be whitelisted based on needs #3434
.filter(attr => attr.type === 'label') .filter(attr => attr.type === 'label')
.map(attr => attr.getPojo()), .map(attr => attr.getPojo()),
attachments: this.getAttachments()
.map(attachment => attachment.getPojo()),
parentNoteIds: this.parents.map(parentNote => parentNote.noteId), parentNoteIds: this.parents.map(parentNote => parentNote.noteId),
childNoteIds: this.children.map(child => child.noteId) childNoteIds: this.children.map(child => child.noteId)
}; };

View File

@ -14,6 +14,8 @@ class Shaca {
this.childParentToBranch = {}; this.childParentToBranch = {};
/** @type {Object.<String, SAttribute>} */ /** @type {Object.<String, SAttribute>} */
this.attributes = {}; this.attributes = {};
/** @type {Object.<String, SAttachment>} */
this.attachments = {};
/** @type {Object.<String, String>} */ /** @type {Object.<String, String>} */
this.aliasToNote = {}; this.aliasToNote = {};
@ -72,6 +74,11 @@ class Shaca {
return this.attributes[attributeId]; return this.attributes[attributeId];
} }
/** @returns {SAttachment|null} */
getAttachment(attachmentId) {
return this.attachments[attachmentId];
}
getEntity(entityName, entityId) { getEntity(entityName, entityId) {
if (!entityName || !entityId) { if (!entityName || !entityId) {
return null; return null;

View File

@ -6,6 +6,7 @@ const log = require('../../services/log');
const SNote = require('./entities/snote'); const SNote = require('./entities/snote');
const SBranch = require('./entities/sbranch'); const SBranch = require('./entities/sbranch');
const SAttribute = require('./entities/sattribute'); const SAttribute = require('./entities/sattribute');
const SAttachment = require("./entities/sattachment");
const shareRoot = require('../share_root'); const shareRoot = require('../share_root');
const eventService = require("../../services/events"); const eventService = require("../../services/events");
@ -65,9 +66,21 @@ function load() {
new SAttribute(row); 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; 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() { function ensureLoad() {