From f4b5d43899599ec1cdb99aa7a4a12a44edbe2af1 Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 29 May 2023 23:42:08 +0200 Subject: [PATCH] inline file attachments when exporting single HTML file --- src/public/app/services/froca.js | 2 -- src/services/export/single.js | 43 +++++++++++++++++++++----------- src/services/export/zip.js | 37 ++++++++++++++++++--------- src/services/ws.js | 10 +++----- 4 files changed, 56 insertions(+), 36 deletions(-) diff --git a/src/public/app/services/froca.js b/src/public/app/services/froca.js index de2ec7192..f978a2fa7 100644 --- a/src/public/app/services/froca.js +++ b/src/public/app/services/froca.js @@ -360,8 +360,6 @@ class Froca { opts.preview = !!opts.preview; const key = `${entityType}-${entityId}-${opts.preview}`; - console.log(key); - if (!this.blobPromises[key]) { this.blobPromises[key] = server.get(`${entityType}/${entityId}/blob?preview=${opts.preview}`) .then(row => new FBlob(row)) diff --git a/src/services/export/single.js b/src/services/export/single.js index 24b18bf75..7d2852b26 100644 --- a/src/services/export/single.js +++ b/src/services/export/single.js @@ -23,13 +23,16 @@ function exportSingleNote(taskContext, branch, format, res) { if (note.type === 'text') { if (format === 'html') { - content = inlineAttachmentImages(content); + content = inlineAttachments(content); if (!content.toLowerCase().includes("${content}`; } - payload = html.prettyPrint(content, {indent_size: 2}); + payload = content.length < 100_000 + ? html.prettyPrint(content, {indent_size: 2}) + : content; + extension = 'html'; mime = 'text/html'; } @@ -61,30 +64,40 @@ function exportSingleNote(taskContext, branch, format, res) { taskContext.taskSucceeded(); } -function inlineAttachmentImages(content) { - const re = /src="[^"]*api\/attachments\/([a-zA-Z0-9_]+)\/image\/?[^"]+"/g; - let match; +function inlineAttachments(content) { + content = content.replace(/src="[^"]*api\/attachments\/([a-zA-Z0-9_]+)\/image\/?[^"]+"/g, (match, attachmentId) => { + const attachment = becca.getAttachment(attachmentId); + if (!attachment || !attachment.mime.startsWith('image/')) { + return match; + } - while (match = re.exec(content)) { - const attachment = becca.getAttachment(match[1]); - if (!attachment) { - continue; + const attachmentContent = attachment.getContent(); + if (!Buffer.isBuffer(attachmentContent)) { + return match; } - if (!attachment.mime.startsWith('image/')) { - continue; + const base64Content = attachmentContent.toString('base64'); + const srcValue = `data:${attachment.mime};base64,${base64Content}`; + + return `src="${srcValue}"`; + }); + + content = content.replace(/href="[^"]*#root[^"]*attachmentId=([a-zA-Z0-9_]+)\/?"/g, (match, attachmentId) => { + const attachment = becca.getAttachment(attachmentId); + if (!attachment) { + return match; } const attachmentContent = attachment.getContent(); if (!Buffer.isBuffer(attachmentContent)) { - continue; + return match; } const base64Content = attachmentContent.toString('base64'); - const srcValue = `data:${attachment.mime};base64,${base64Content}`; + const hrefValue = `data:${attachment.mime};base64,${base64Content}`; - content = content.replaceAll(match[0], `src="${srcValue}"`); - } + return `href="${hrefValue}" download="${utils.escapeHtml(attachment.title)}"`; + }); return content; } diff --git a/src/services/export/zip.js b/src/services/export/zip.js index a64bbe2c3..91ec8528a 100644 --- a/src/services/export/zip.js +++ b/src/services/export/zip.js @@ -280,26 +280,37 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) }); content = content.replace(/src="[^"]*api\/attachments\/([a-zA-Z0-9_]+)\/image\/[^"]*"/g, (match, targetAttachmentId) => { - let url; - - const attachmentMeta = noteMeta.attachments.find(attMeta => attMeta.attachmentId === targetAttachmentId); - if (attachmentMeta) { - // easy job here, because attachment will be in the same directory as the note's data file. - url = attachmentMeta.dataFileName; - } else { - log.info(`Could not find attachment meta object for attachmentId '${targetAttachmentId}'`); - } + const url = findAttachment(targetAttachmentId); return url ? `src="${url}"` : match; }); - content = content.replace(/href="[^"]*#root[a-zA-Z0-9_\/]*\/([a-zA-Z0-9_]+)\/?"/g, (match, targetNoteId) => { + content = content.replace(/href="[^"]*#root[^"]*attachmentId=([a-zA-Z0-9_]+)\/?"/g, (match, targetAttachmentId) => { + const url = findAttachment(targetAttachmentId); + + return url ? `href="${url}"` : match; + }); + + content = content.replace(/href="[^"]*#root[a-zA-Z0-9_\/]*\/([a-zA-Z0-9_]+)[^"]*"/g, (match, targetNoteId) => { const url = getNoteTargetUrl(targetNoteId, noteMeta); return url ? `href="${url}"` : match; }); return content; + + function findAttachment(targetAttachmentId) { + let url; + + const attachmentMeta = noteMeta.attachments.find(attMeta => attMeta.attachmentId === targetAttachmentId); + if (attachmentMeta) { + // easy job here, because attachment will be in the same directory as the note's data file. + url = attachmentMeta.dataFileName; + } else { + log.info(`Could not find attachment meta object for attachmentId '${targetAttachmentId}'`); + } + return url; + } } /** @@ -339,7 +350,7 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) `; } - return content.length < 100000 + return content.length < 100_000 ? html.prettyPrint(content, {indent_size: 2}) : content; } else if (noteMeta.format === 'markdown') { @@ -451,7 +462,9 @@ ${markdownContent}`; `; - const prettyHtml = html.prettyPrint(fullHtml, {indent_size: 2}); + const prettyHtml = fullHtml.length < 100_000 + ? html.prettyPrint(fullHtml, {indent_size: 2}) + : fullHtml; archive.append(prettyHtml, { name: navigationMeta.dataFileName }); } diff --git a/src/services/ws.js b/src/services/ws.js index cb8b132ef..d0ca24edc 100644 --- a/src/services/ws.js +++ b/src/services/ws.js @@ -103,8 +103,8 @@ function fillInAdditionalProperties(entityChange) { } // fill in some extra data needed by the frontend - // first try to use becca which works for non-deleted entities - // only when that fails try to load from database + // first try to use becca, which works for non-deleted entities + // only when that fails, try to load from the database if (entityChange.entityName === 'attributes') { entityChange.entity = becca.getAttribute(entityChange.entityId); @@ -150,11 +150,7 @@ function fillInAdditionalProperties(entityChange) { } else if (entityChange.entityName === 'blobs') { entityChange.noteIds = sql.getColumn("SELECT noteId FROM notes WHERE blobId = ? AND isDeleted = 0", [entityChange.entityId]); } else if (entityChange.entityName === 'attachments') { - entityChange.entity = sql.getRow(` - SELECT attachments.*, LENGTH(blobs.content) - FROM attachments - JOIN blobs ON blobs.blobId = attachments.blobId - WHERE attachmentId = ?`, [entityChange.entityId]); + entityChange.entity = becca.getAttachment(entityChange.entityId, {includeContentLength: true}); } if (entityChange.entity instanceof AbstractBeccaEntity) {