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 | ||||
|  | ||||
| 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" | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user