From bd8568809fabd160967005f258e30b7bb811cc4e Mon Sep 17 00:00:00 2001 From: zadam Date: Wed, 25 Jan 2023 09:55:29 +0100 Subject: [PATCH] export/import attachments --- bin/export-schema.sh | 2 +- db/schema.sql | 20 +++++++- src/becca/entities/bnote_attachment.js | 2 +- src/services/export/zip.js | 63 +++++++++++++++++++++----- src/services/import/zip.js | 37 +++++++++++++-- 5 files changed, 106 insertions(+), 18 deletions(-) diff --git a/bin/export-schema.sh b/bin/export-schema.sh index 152bcd0e0..ab5de1a81 100755 --- a/bin/export-schema.sh +++ b/bin/export-schema.sh @@ -2,6 +2,6 @@ SCHEMA_FILE_PATH=db/schema.sql -sqlite3 ~/trilium-data/document.db .schema | grep -v "sqlite_sequence" > "$SCHEMA_FILE_PATH" +sqlite3 ./data/document.db .schema | grep -v "sqlite_sequence" > "$SCHEMA_FILE_PATH" echo "DB schema exported to $SCHEMA_FILE_PATH" \ No newline at end of file diff --git a/db/schema.sql b/db/schema.sql index d1d2554f5..d4de3b8e2 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -61,7 +61,7 @@ CREATE TABLE IF NOT EXISTS "note_revisions" (`noteRevisionId` TEXT NOT NULL PRIM `dateLastEdited` TEXT NOT NULL, `dateCreated` TEXT NOT NULL); CREATE TABLE IF NOT EXISTS "note_revision_contents" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY, - `content` TEXT DEFAULT NULL, + `content` TEXT, `utcDateModified` TEXT NOT NULL); CREATE TABLE IF NOT EXISTS "options" ( @@ -112,3 +112,21 @@ CREATE TABLE IF NOT EXISTS "recent_notes" notePath TEXT not null, utcDateCreated TEXT not null ); +CREATE TABLE IF NOT EXISTS "note_attachments" +( + noteAttachmentId TEXT not null primary key, + noteId TEXT not null, + name TEXT not null, + mime TEXT not null, + isProtected INT not null DEFAULT 0, + contentCheckSum TEXT not null, + utcDateModified TEXT not null, + isDeleted INT not null, + `deleteId` TEXT DEFAULT NULL); +CREATE TABLE IF NOT EXISTS "note_attachment_contents" (`noteAttachmentId` TEXT NOT NULL PRIMARY KEY, + `content` TEXT DEFAULT NULL, + `utcDateModified` TEXT NOT NULL); +CREATE INDEX IDX_note_attachments_name + on note_attachments (name); +CREATE UNIQUE INDEX IDX_note_attachments_noteId_name + on note_attachments (noteId, name); diff --git a/src/becca/entities/bnote_attachment.js b/src/becca/entities/bnote_attachment.js index e72faaf6a..0fd8bc246 100644 --- a/src/becca/entities/bnote_attachment.js +++ b/src/becca/entities/bnote_attachment.js @@ -91,7 +91,7 @@ class BNoteAttachment extends AbstractBeccaEntity { setContent(content) { this.contentCheckSum = this.calculateCheckSum(content); - this.save(); + this.save(); // also explicitly save note_attachment to update contentCheckSum const pojo = { noteAttachmentId: this.noteAttachmentId, diff --git a/src/services/export/zip.js b/src/services/export/zip.js index 3db3cf5f4..365794d30 100644 --- a/src/services/export/zip.js +++ b/src/services/export/zip.js @@ -58,7 +58,7 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) } } - function getDataFileName(note, baseFileName, existingFileNames) { + function getDataFileName(type, mime, baseFileName, existingFileNames) { let fileName = baseFileName; let existingExtension = path.extname(fileName).toLowerCase(); @@ -70,24 +70,25 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) // following two are handled specifically since we always want to have these extensions no matter the automatic detection // and/or existing detected extensions in the note name - if (note.type === 'text' && format === 'markdown') { + if (type === 'text' && format === 'markdown') { newExtension = 'md'; } - else if (note.type === 'text' && format === 'html') { + else if (type === 'text' && format === 'html') { newExtension = 'html'; } - else if (note.mime === 'application/x-javascript' || note.mime === 'text/javascript') { + else if (mime === 'application/x-javascript' || mime === 'text/javascript') { newExtension = 'js'; } else if (existingExtension.length > 0) { // if the page already has an extension, then we'll just keep it newExtension = null; } else { - if (note.mime?.toLowerCase()?.trim() === "image/jpg") { + if (mime?.toLowerCase()?.trim() === "image/jpg") { newExtension = 'jpg'; - } - else { - newExtension = mimeTypes.extension(note.mime) || "dat"; + } else if (mime?.toLowerCase()?.trim() === "text/mermaid") { + newExtension = 'txt'; + } else { + newExtension = mimeTypes.extension(mime) || "dat"; } } @@ -166,7 +167,25 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) // if it's a leaf then we'll export it even if it's empty if (available && (note.getContent().length > 0 || childBranches.length === 0)) { - meta.dataFileName = getDataFileName(note, baseFileName, existingFileNames); + meta.dataFileName = getDataFileName(note.type, note.mime, baseFileName, existingFileNames); + } + + const attachments = note.getNoteAttachments(); + + if (attachments.length > 0) { + meta.attachments = attachments + .filter(attachment => ["canvasSvg", "mermaidSvg"].includes(attachment.name)) + .map(attachment => ({ + + name: attachment.name, + mime: attachment.mime, + dataFileName: getDataFileName( + null, + attachment.mime, + baseFileName + "_" + attachment.name, + existingFileNames + ) + })); } if (childBranches.length > 0) { @@ -215,8 +234,15 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) const meta = noteIdToMeta[targetPath[targetPath.length - 1]]; - // link can target note which is only "folder-note" and as such will not have a file in an export - url += encodeURIComponent(meta.dataFileName || meta.dirFileName); + // for some note types it's more user-friendly to see the attachment (if exists) instead of source note + const preferredAttachment = (meta.attachments || []).find(attachment => ['mermaidSvg', 'canvasSvg'].includes(attachment.name)); + + if (preferredAttachment) { + url += encodeURIComponent(preferredAttachment.dataFileName); + } else { + // link can target note which is only "folder-note" and as such will not have a file in an export + url += encodeURIComponent(meta.dataFileName || meta.dirFileName); + } return url; } @@ -310,11 +336,24 @@ ${markdownContent}`; if (noteMeta.dataFileName) { const content = prepareContent(noteMeta.title, note.getContent(), noteMeta); - archive.append(content, { name: filePathPrefix + noteMeta.dataFileName, date: dateUtils.parseDateTime(note.utcDateModified) }); + archive.append(content, { + name: filePathPrefix + noteMeta.dataFileName, + date: dateUtils.parseDateTime(note.utcDateModified) + }); } taskContext.increaseProgressCount(); + for (const attachmentMeta of noteMeta.attachments || []) { + const noteAttachment = note.getNoteAttachmentByName(attachmentMeta.name); + const content = noteAttachment.getContent(); + + archive.append(content, { + name: filePathPrefix + attachmentMeta.dataFileName, + date: dateUtils.parseDateTime(note.utcDateModified) + }); + } + if (noteMeta.children && noteMeta.children.length > 0) { const directoryPath = filePathPrefix + noteMeta.dirFileName; diff --git a/src/services/import/zip.js b/src/services/import/zip.js index d598ab674..25fa5457f 100644 --- a/src/services/import/zip.js +++ b/src/services/import/zip.js @@ -14,6 +14,7 @@ const treeService = require("../tree"); const yauzl = require("yauzl"); const htmlSanitizer = require('../html_sanitizer'); const becca = require("../../becca/becca"); +const BNoteAttachment = require("../../becca/entities/bnote_attachment"); /** * @param {TaskContext} taskContext @@ -64,6 +65,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) { }; let parent; + let attachmentMeta = false; for (const segment of pathSegments) { if (!cursor || !cursor.children || cursor.children.length === 0) { @@ -71,12 +73,29 @@ async function importZip(taskContext, fileBuffer, importRootNote) { } parent = cursor; - cursor = cursor.children.find(file => file.dataFileName === segment || file.dirFileName === segment); + cursor = parent.children.find(file => file.dataFileName === segment || file.dirFileName === segment); + + if (!cursor) { + for (const file of parent.children) { + for (const attachment of file.attachments || []) { + if (attachment.dataFileName === segment) { + cursor = file; + attachmentMeta = attachment; + break; + } + } + + if (cursor) { + break; + } + } + } } return { parentNoteMeta: parent, - noteMeta: cursor + noteMeta: cursor, + attachmentMeta }; } @@ -354,13 +373,25 @@ async function importZip(taskContext, fileBuffer, importRootNote) { } function saveNote(filePath, content) { - const {parentNoteMeta, noteMeta} = getMeta(filePath); + const {parentNoteMeta, noteMeta, attachmentMeta} = getMeta(filePath); if (noteMeta?.noImport) { return; } const noteId = getNoteId(noteMeta, filePath); + + if (attachmentMeta) { + const noteAttachment = new BNoteAttachment({ + noteId, + name: attachmentMeta.name, + mime: attachmentMeta.mime + }); + + noteAttachment.setContent(content); + return; + } + const parentNoteId = getParentNoteId(filePath, parentNoteMeta); if (!parentNoteId) {