From 1fdb23746a5f14901112c32eba87cb4bbe777a55 Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 16 Mar 2023 18:34:39 +0100 Subject: [PATCH] uploading image to attachment --- db/migrations/0217__attachments.sql | 4 +-- db/schema.sql | 5 +-- src/becca/entities/battachment.js | 13 +++----- src/becca/entities/bnote.js | 8 +++-- src/routes/api/image.js | 35 ++++++++++++++++----- src/routes/routes.js | 1 + src/services/image.js | 48 +++++++++++++++++++++++++++-- src/services/utils.js | 2 +- 8 files changed, 90 insertions(+), 26 deletions(-) diff --git a/db/migrations/0217__attachments.sql b/db/migrations/0217__attachments.sql index 2958aa2d2..8d87de901 100644 --- a/db/migrations/0217__attachments.sql +++ b/db/migrations/0217__attachments.sql @@ -6,11 +6,11 @@ CREATE TABLE IF NOT EXISTS "attachments" mime TEXT not null, title TEXT not null, isProtected INT not null DEFAULT 0, - blobId TEXT not null, + blobId TEXT DEFAULT null, utcDateScheduledForDeletionSince TEXT DEFAULT NULL, utcDateModified TEXT not null, isDeleted INT not null, deleteId TEXT DEFAULT NULL); -CREATE UNIQUE INDEX IDX_attachments_parentId_role +CREATE INDEX IDX_attachments_parentId_role on attachments (parentId, role); diff --git a/db/schema.sql b/db/schema.sql index 5b8a8379b..2dafcdbd1 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -112,9 +112,10 @@ CREATE TABLE IF NOT EXISTS "attachments" mime TEXT not null, title TEXT not null, isProtected INT not null DEFAULT 0, - blobId TEXT not null, + blobId TEXT DEFAULT null, + utcDateScheduledForDeletionSince TEXT DEFAULT NULL, utcDateModified TEXT not null, isDeleted INT not null, deleteId TEXT DEFAULT NULL); -CREATE UNIQUE INDEX IDX_attachments_parentId_role +CREATE INDEX IDX_attachments_parentId_role on attachments (parentId, role); diff --git a/src/becca/entities/battachment.js b/src/becca/entities/battachment.js index fd82ac9a5..b653b2233 100644 --- a/src/becca/entities/battachment.js +++ b/src/becca/entities/battachment.js @@ -21,7 +21,7 @@ class BAttachment extends AbstractBeccaEntity { super(); if (!row.parentId?.trim()) { - throw new Error("'noteId' must be given to initialize a Attachment entity"); + throw new Error("'parentId' must be given to initialize a Attachment entity"); } else if (!row.role?.trim()) { throw new Error("'role' must be given to initialize a Attachment entity"); } else if (!row.mime?.trim()) { @@ -40,6 +40,8 @@ class BAttachment extends AbstractBeccaEntity { this.mime = row.mime; /** @type {string} */ this.title = row.title; + /** @type {string} */ + this.blobId = row.blobId; /** @type {boolean} */ this.isProtected = !!row.isProtected; /** @type {string} */ @@ -71,15 +73,7 @@ class BAttachment extends AbstractBeccaEntity { this._setContent(content, opts); } - calculateCheckSum(content) { - return utils.hash(`${this.attachmentId}|${content.toString()}`); - } - beforeSaving() { - if (!this.name.match(/^[a-z0-9]+$/i)) { - throw new Error(`Name must be alphanumerical, "${this.name}" given.`); - } - super.beforeSaving(); this.utcDateModified = dateUtils.utcNowDateTime(); @@ -92,6 +86,7 @@ class BAttachment extends AbstractBeccaEntity { role: this.role, mime: this.mime, title: this.title, + blobId: this.blobId, isProtected: !!this.isProtected, isDeleted: false, utcDateScheduledForDeletionSince: this.utcDateScheduledForDeletionSince, diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js index aa1be2577..d75325d77 100644 --- a/src/becca/entities/bnote.js +++ b/src/becca/entities/bnote.js @@ -1429,7 +1429,7 @@ class BNote extends AbstractBeccaEntity { } } else { attachment = new BAttachment({ - noteId: this.noteId, + parentId: this.noteId, title, role, mime, @@ -1437,7 +1437,11 @@ class BNote extends AbstractBeccaEntity { }); } - attachment.setContent(content, { forceSave: true }); + if (content !== undefined && content !== null) { + attachment.setContent(content, {forceSave: true}); + } else { + attachment.save(); + } return attachment; } diff --git a/src/routes/api/image.js b/src/routes/api/image.js index adf3a6441..169691d42 100644 --- a/src/routes/api/image.js +++ b/src/routes/api/image.js @@ -11,15 +11,12 @@ function returnImage(req, res) { const image = becca.getNote(req.params.noteId); if (!image) { - return res.sendStatus(404); + res.set('Content-Type', 'image/png'); + return res.send(fs.readFileSync(`${RESOURCE_DIR}/db/image-deleted.png`)); } else if (!["image", "canvas"].includes(image.type)){ return res.sendStatus(400); } - else if (image.isDeleted || image.data === null) { - res.set('Content-Type', 'image/png'); - return res.send(fs.readFileSync(`${RESOURCE_DIR}/db/image-deleted.png`)); - } /** * special "image" type. the canvas is actually type application/json @@ -46,6 +43,29 @@ function returnImage(req, res) { } } +function returnAttachedImage(req, res) { + const note = becca.getNote(req.params.noteId); + + if (!note) { + return res.sendStatus(404); + } + + const attachment = becca.getAttachment(req.params.attachmentId); + + if (!attachment || attachment.parentId !== note.noteId) { + res.set('Content-Type', 'image/png'); + return res.send(fs.readFileSync(`${RESOURCE_DIR}/db/image-deleted.png`)); + } + + if (!["image"].includes(attachment.role)) { + return res.sendStatus(400); + } + + res.set('Content-Type', attachment.mime); + res.set("Cache-Control", "no-cache, no-store, must-revalidate"); + res.send(attachment.getContent()); +} + function uploadImage(req) { const {noteId} = req.query; const {file} = req; @@ -57,10 +77,10 @@ function uploadImage(req) { } if (!["image/png", "image/jpg", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"].includes(file.mimetype)) { - throw new ValidationError(`Unknown image type: ${file.mimetype}`); + throw new ValidationError(`Unknown image type '${file.mimetype}'`); } - const {url} = imageService.saveImage(noteId, file.buffer, file.originalname, true, true); + const {url} = imageService.saveImageToAttachment(noteId, file.buffer, file.originalname, true, true); return { uploaded: true, @@ -92,6 +112,7 @@ function updateImage(req) { module.exports = { returnImage, + returnAttachedImage, uploadImage, updateImage }; diff --git a/src/routes/routes.js b/src/routes/routes.js index 8a91952ab..e6e82d231 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -195,6 +195,7 @@ function register(app) { // :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/notes/:noteId/images/:attachmentId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnAttachedImage); route(POST, '/api/images', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], imageRoute.uploadImage, apiResultHandler); route(PUT, '/api/images/:noteId', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], imageRoute.updateImage, apiResultHandler); diff --git a/src/services/image.js b/src/services/image.js index ac9d9cfd1..0edc0b548 100644 --- a/src/services/image.js +++ b/src/services/image.js @@ -12,6 +12,8 @@ const sanitizeFilename = require('sanitize-filename'); const isSvg = require('is-svg'); const isAnimated = require('is-animated'); const htmlSanitizer = require("./html_sanitizer"); +const {attach} = require("jsdom/lib/jsdom/living/helpers/svg/basic-types.js"); +const NotFoundError = require("../errors/not_found_error.js"); async function processImage(uploadBuffer, originalName, shrinkImageSwitch) { const compressImages = optionService.getOptionBool("compressImages"); @@ -119,9 +121,7 @@ function saveImage(parentNoteId, uploadBuffer, originalName, shrinkImageSwitch, note.title = sanitizeFilename(originalName); } - note.save(); - - note.setContent(buffer); + note.setContent(buffer, { forceSave: true }); }); }); @@ -133,6 +133,47 @@ function saveImage(parentNoteId, uploadBuffer, originalName, shrinkImageSwitch, }; } +function saveImageToAttachment(noteId, uploadBuffer, originalName, shrinkImageSwitch, trimFilename = false) { + log.info(`Saving image '${originalName}' as attachment into note '${noteId}'`); + + if (trimFilename && originalName.length > 40) { + // https://github.com/zadam/trilium/issues/2307 + originalName = "image"; + } + + const fileName = sanitizeFilename(originalName); + const note = becca.getNote(noteId); + + if (!note) { + throw new NotFoundError(`Could not find note '${noteId}'`); + } + + const attachment = note.saveAttachment({ + role: 'image', + mime: 'unknown', + title: fileName + }); + + // resizing images asynchronously since JIMP does not support sync operation + processImage(uploadBuffer, originalName, shrinkImageSwitch).then(({buffer, imageFormat}) => { + sql.transactional(() => { + attachment.mime = getImageMimeFromExtension(imageFormat.ext); + + if (!originalName.includes(".")) { + originalName += `.${imageFormat.ext}`; + attachment.title = sanitizeFilename(originalName); + } + + attachment.setContent(buffer, { forceSave: true }); + }); + }); + + return { + attachment, + url: `api/notes/${note.noteId}/images/${attachment.attachmentId}/${encodeURIComponent(fileName)}` + }; +} + async function shrinkImage(buffer, originalName) { let jpegQuality = optionService.getOptionInt('imageJpegQuality'); @@ -187,5 +228,6 @@ async function resize(buffer, quality) { module.exports = { saveImage, + saveImageToAttachment, updateImage }; diff --git a/src/services/utils.js b/src/services/utils.js index 432d69013..5a9cdb7a5 100644 --- a/src/services/utils.js +++ b/src/services/utils.js @@ -165,7 +165,7 @@ function sanitizeFilenameForHeader(filename) { sanitizedFilename = "file"; } - return encodeURIComponent(sanitizedFilename) + return encodeURIComponent(sanitizedFilename); } function getContentDisposition(filename) {