mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	export/import attachments
This commit is contained in:
		| @@ -2,6 +2,6 @@ | |||||||
|  |  | ||||||
| SCHEMA_FILE_PATH=db/schema.sql | 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" | echo "DB schema exported to $SCHEMA_FILE_PATH" | ||||||
| @@ -61,7 +61,7 @@ CREATE TABLE IF NOT EXISTS "note_revisions" (`noteRevisionId`	TEXT NOT NULL PRIM | |||||||
|                                              `dateLastEdited` TEXT NOT NULL, |                                              `dateLastEdited` TEXT NOT NULL, | ||||||
|                                              `dateCreated` TEXT NOT NULL); |                                              `dateCreated` TEXT NOT NULL); | ||||||
| CREATE TABLE IF NOT EXISTS "note_revision_contents" (`noteRevisionId`	TEXT NOT NULL PRIMARY KEY, | CREATE TABLE IF NOT EXISTS "note_revision_contents" (`noteRevisionId`	TEXT NOT NULL PRIMARY KEY, | ||||||
|                                                      `content`	TEXT DEFAULT NULL, |                                                      `content`	TEXT, | ||||||
|                                                      `utcDateModified` TEXT NOT NULL); |                                                      `utcDateModified` TEXT NOT NULL); | ||||||
| CREATE TABLE IF NOT EXISTS "options" | CREATE TABLE IF NOT EXISTS "options" | ||||||
| ( | ( | ||||||
| @@ -112,3 +112,21 @@ CREATE TABLE IF NOT EXISTS "recent_notes" | |||||||
|     notePath TEXT not null, |     notePath TEXT not null, | ||||||
|     utcDateCreated 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); | ||||||
|   | |||||||
| @@ -91,7 +91,7 @@ class BNoteAttachment extends AbstractBeccaEntity { | |||||||
|  |  | ||||||
|     setContent(content) { |     setContent(content) { | ||||||
|         this.contentCheckSum = this.calculateCheckSum(content); |         this.contentCheckSum = this.calculateCheckSum(content); | ||||||
|         this.save(); |         this.save(); // also explicitly save note_attachment to update contentCheckSum | ||||||
|  |  | ||||||
|         const pojo = { |         const pojo = { | ||||||
|             noteAttachmentId: this.noteAttachmentId, |             noteAttachmentId: this.noteAttachmentId, | ||||||
|   | |||||||
| @@ -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 fileName = baseFileName; | ||||||
|  |  | ||||||
|         let existingExtension = path.extname(fileName).toLowerCase(); |         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 |         // 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 |         // and/or existing detected extensions in the note name | ||||||
|         if (note.type === 'text' && format === 'markdown') { |         if (type === 'text' && format === 'markdown') { | ||||||
|             newExtension = 'md'; |             newExtension = 'md'; | ||||||
|         } |         } | ||||||
|         else if (note.type === 'text' && format === 'html') { |         else if (type === 'text' && format === 'html') { | ||||||
|             newExtension = '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'; |             newExtension = 'js'; | ||||||
|         } |         } | ||||||
|         else if (existingExtension.length > 0) { // if the page already has an extension, then we'll just keep it |         else if (existingExtension.length > 0) { // if the page already has an extension, then we'll just keep it | ||||||
|             newExtension = null; |             newExtension = null; | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|             if (note.mime?.toLowerCase()?.trim() === "image/jpg") { |             if (mime?.toLowerCase()?.trim() === "image/jpg") { | ||||||
|                 newExtension = 'jpg'; |                 newExtension = 'jpg'; | ||||||
|             } |             } else if (mime?.toLowerCase()?.trim() === "text/mermaid") { | ||||||
|             else { |                 newExtension = 'txt'; | ||||||
|                 newExtension = mimeTypes.extension(note.mime) || "dat"; |             } 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 it's a leaf then we'll export it even if it's empty | ||||||
|         if (available && (note.getContent().length > 0 || childBranches.length === 0)) { |         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) { |         if (childBranches.length > 0) { | ||||||
| @@ -215,8 +234,15 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) | |||||||
|  |  | ||||||
|         const meta = noteIdToMeta[targetPath[targetPath.length - 1]]; |         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 |         // for some note types it's more user-friendly to see the attachment (if exists) instead of source note | ||||||
|         url += encodeURIComponent(meta.dataFileName || meta.dirFileName); |         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; |         return url; | ||||||
|     } |     } | ||||||
| @@ -310,11 +336,24 @@ ${markdownContent}`; | |||||||
|         if (noteMeta.dataFileName) { |         if (noteMeta.dataFileName) { | ||||||
|             const content = prepareContent(noteMeta.title, note.getContent(), noteMeta); |             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(); |         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) { |         if (noteMeta.children && noteMeta.children.length > 0) { | ||||||
|             const directoryPath = filePathPrefix + noteMeta.dirFileName; |             const directoryPath = filePathPrefix + noteMeta.dirFileName; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ const treeService = require("../tree"); | |||||||
| const yauzl = require("yauzl"); | const yauzl = require("yauzl"); | ||||||
| const htmlSanitizer = require('../html_sanitizer'); | const htmlSanitizer = require('../html_sanitizer'); | ||||||
| const becca = require("../../becca/becca"); | const becca = require("../../becca/becca"); | ||||||
|  | const BNoteAttachment = require("../../becca/entities/bnote_attachment"); | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @param {TaskContext} taskContext |  * @param {TaskContext} taskContext | ||||||
| @@ -64,6 +65,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) { | |||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         let parent; |         let parent; | ||||||
|  |         let attachmentMeta = false; | ||||||
|  |  | ||||||
|         for (const segment of pathSegments) { |         for (const segment of pathSegments) { | ||||||
|             if (!cursor || !cursor.children || cursor.children.length === 0) { |             if (!cursor || !cursor.children || cursor.children.length === 0) { | ||||||
| @@ -71,12 +73,29 @@ async function importZip(taskContext, fileBuffer, importRootNote) { | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             parent = cursor; |             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 { |         return { | ||||||
|             parentNoteMeta: parent, |             parentNoteMeta: parent, | ||||||
|             noteMeta: cursor |             noteMeta: cursor, | ||||||
|  |             attachmentMeta | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -354,13 +373,25 @@ async function importZip(taskContext, fileBuffer, importRootNote) { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     function saveNote(filePath, content) { |     function saveNote(filePath, content) { | ||||||
|         const {parentNoteMeta, noteMeta} = getMeta(filePath); |         const {parentNoteMeta, noteMeta, attachmentMeta} = getMeta(filePath); | ||||||
|  |  | ||||||
|         if (noteMeta?.noImport) { |         if (noteMeta?.noImport) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const noteId = getNoteId(noteMeta, filePath); |         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); |         const parentNoteId = getParentNoteId(filePath, parentNoteMeta); | ||||||
|  |  | ||||||
|         if (!parentNoteId) { |         if (!parentNoteId) { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user