mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 20:06:08 +01:00 
			
		
		
		
	uploading image to attachment
This commit is contained in:
		@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -165,7 +165,7 @@ function sanitizeFilenameForHeader(filename) {
 | 
			
		||||
        sanitizedFilename = "file";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return encodeURIComponent(sanitizedFilename)
 | 
			
		||||
    return encodeURIComponent(sanitizedFilename);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getContentDisposition(filename) {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user