mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	import/export logic refactored into separate files per format, closes #237
This commit is contained in:
		| @@ -6,7 +6,6 @@ import exportService from "../services/export.js"; | ||||
| const $dialog = $("#export-subtree-dialog"); | ||||
| const $form = $("#export-subtree-form"); | ||||
| const $noteTitle = $dialog.find(".note-title"); | ||||
| const $exportFormat = $dialog.find("input[name='export-format']:checked"); | ||||
|  | ||||
| async function showDialog() { | ||||
|     glob.activeDialog = $dialog; | ||||
| @@ -20,7 +19,7 @@ async function showDialog() { | ||||
| } | ||||
|  | ||||
| $form.submit(() => { | ||||
|     const exportFormat = $exportFormat.val(); | ||||
|     const exportFormat = $dialog.find("input[name='export-format']:checked").val(); | ||||
|  | ||||
|     const currentNode = treeService.getCurrentNode(); | ||||
|  | ||||
|   | ||||
| @@ -1,270 +1,34 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const html = require('html'); | ||||
| const tar = require('tar-stream'); | ||||
| const sanitize = require("sanitize-filename"); | ||||
| const nativeTarExportService = require('../../services/export/native_tar'); | ||||
| const markdownTarExportService = require('../../services/export/markdown_tar'); | ||||
| const markdownSingleExportService = require('../../services/export/markdown_single'); | ||||
| const opmlExportService = require('../../services/export/opml'); | ||||
| const repository = require("../../services/repository"); | ||||
| const utils = require('../../services/utils'); | ||||
| const TurndownService = require('turndown'); | ||||
|  | ||||
| async function exportNote(req, res) { | ||||
|     // entityId maybe either noteId or branchId depending on format | ||||
|     const entityId = req.params.entityId; | ||||
|     const format = req.params.format; | ||||
|  | ||||
|     if (format === 'tar') { | ||||
|         await exportToTar(await repository.getBranch(entityId), res); | ||||
|     if (format === 'native-tar') { | ||||
|         await nativeTarExportService.exportToTar(await repository.getBranch(entityId), res); | ||||
|     } | ||||
|     else if (format === 'opml') { | ||||
|         await exportToOpml(await repository.getBranch(entityId), res); | ||||
|     } | ||||
|     else if (format === 'markdown') { | ||||
|         await exportToMarkdown(await repository.getBranch(entityId), res); | ||||
|     else if (format === 'markdown-tar') { | ||||
|         await markdownTarExportService.exportToMarkdown(await repository.getBranch(entityId), res); | ||||
|     } | ||||
|     // export single note without subtree | ||||
|     else if (format === 'markdown-single') { | ||||
|         await exportSingleMarkdown(await repository.getNote(entityId), res); | ||||
|         await markdownSingleExportService.exportSingleMarkdown(await repository.getNote(entityId), res); | ||||
|     } | ||||
|     else if (format === 'opml') { | ||||
|         await opmlExportService.exportToOpml(await repository.getBranch(entityId), res); | ||||
|     } | ||||
|     else { | ||||
|         return [404, "Unrecognized export format " + format]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| function escapeXmlAttribute(text) { | ||||
|     return text.replace(/&/g, '&') | ||||
|         .replace(/</g, '<') | ||||
|         .replace(/>/g, '>') | ||||
|         .replace(/"/g, '"') | ||||
|         .replace(/'/g, '''); | ||||
| } | ||||
|  | ||||
| function prepareText(text) { | ||||
|     const newLines = text.replace(/(<p[^>]*>|<br\s*\/?>)/g, '\n') | ||||
|                          .replace(/ /g, ' '); // nbsp isn't in XML standard (only HTML) | ||||
|  | ||||
|     const stripped = utils.stripTags(newLines); | ||||
|  | ||||
|     const escaped = escapeXmlAttribute(stripped); | ||||
|  | ||||
|     return escaped.replace(/\n/g, '
'); | ||||
| } | ||||
|  | ||||
| async function exportToOpml(branch, res) { | ||||
|     const note = await branch.getNote(); | ||||
|     const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title; | ||||
|     const sanitizedTitle = sanitize(title); | ||||
|  | ||||
|     async function exportNoteInner(branchId) { | ||||
|         const branch = await repository.getBranch(branchId); | ||||
|         const note = await branch.getNote(); | ||||
|         const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title; | ||||
|  | ||||
|         const preparedTitle = prepareText(title); | ||||
|         const preparedContent = prepareText(note.content); | ||||
|  | ||||
|         res.write(`<outline title="${preparedTitle}" text="${preparedContent}">\n`); | ||||
|  | ||||
|         for (const child of await note.getChildBranches()) { | ||||
|             await exportNoteInner(child.branchId); | ||||
|         } | ||||
|  | ||||
|         res.write('</outline>'); | ||||
|     } | ||||
|  | ||||
|     res.setHeader('Content-Disposition', 'file; filename="' + sanitizedTitle + '.opml"'); | ||||
|     res.setHeader('Content-Type', 'text/x-opml'); | ||||
|  | ||||
|     res.write(`<?xml version="1.0" encoding="UTF-8"?> | ||||
| <opml version="1.0"> | ||||
| <head> | ||||
| <title>Trilium export</title> | ||||
| </head> | ||||
| <body>`); | ||||
|  | ||||
|     await exportNoteInner(branch.branchId); | ||||
|  | ||||
|     res.write(`</body> | ||||
| </opml>`); | ||||
|     res.end(); | ||||
| } | ||||
|  | ||||
| async function exportToTar(branch, res) { | ||||
|     const pack = tar.pack(); | ||||
|  | ||||
|     const exportedNoteIds = []; | ||||
|     const name = await exportNoteInner(branch, ''); | ||||
|  | ||||
|     async function exportNoteInner(branch, directory) { | ||||
|         const note = await branch.getNote(); | ||||
|         const childFileName = directory + sanitize(note.title); | ||||
|  | ||||
|         if (exportedNoteIds.includes(note.noteId)) { | ||||
|             saveMetadataFile(childFileName, { | ||||
|                 version: 1, | ||||
|                 clone: true, | ||||
|                 noteId: note.noteId, | ||||
|                 prefix: branch.prefix | ||||
|             }); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const metadata = { | ||||
|             version: 1, | ||||
|             clone: false, | ||||
|             noteId: note.noteId, | ||||
|             title: note.title, | ||||
|             prefix: branch.prefix, | ||||
|             isExpanded: branch.isExpanded, | ||||
|             type: note.type, | ||||
|             mime: note.mime, | ||||
|             // we don't export dateCreated and dateModified of any entity since that would be a bit misleading | ||||
|             attributes: (await note.getOwnedAttributes()).map(attribute => { | ||||
|                 return { | ||||
|                     type: attribute.type, | ||||
|                     name: attribute.name, | ||||
|                     value: attribute.value, | ||||
|                     isInheritable: attribute.isInheritable, | ||||
|                     position: attribute.position | ||||
|                 }; | ||||
|             }), | ||||
|             links: (await note.getLinks()).map(link => { | ||||
|                 return { | ||||
|                     type: link.type, | ||||
|                     targetNoteId: link.targetNoteId | ||||
|                 } | ||||
|             }) | ||||
|         }; | ||||
|  | ||||
|         if (await note.hasLabel('excludeFromExport')) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         saveMetadataFile(childFileName, metadata); | ||||
|         saveDataFile(childFileName, note); | ||||
|  | ||||
|         exportedNoteIds.push(note.noteId); | ||||
|  | ||||
|         const childBranches = await note.getChildBranches(); | ||||
|  | ||||
|         if (childBranches.length > 0) { | ||||
|             saveDirectory(childFileName); | ||||
|         } | ||||
|  | ||||
|         for (const childBranch of childBranches) { | ||||
|             await exportNoteInner(childBranch, childFileName + "/"); | ||||
|         } | ||||
|  | ||||
|         return childFileName; | ||||
|     } | ||||
|  | ||||
|     function saveDataFile(childFileName, note) { | ||||
|         const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content; | ||||
|  | ||||
|         pack.entry({name: childFileName + ".dat", size: content.length}, content); | ||||
|     } | ||||
|  | ||||
|     function saveMetadataFile(childFileName, metadata) { | ||||
|         const metadataJson = JSON.stringify(metadata, null, '\t'); | ||||
|  | ||||
|         pack.entry({name: childFileName + ".meta", size: metadataJson.length}, metadataJson); | ||||
|     } | ||||
|  | ||||
|     function saveDirectory(childFileName) { | ||||
|         pack.entry({name: childFileName, type: 'directory'}); | ||||
|     } | ||||
|  | ||||
|     pack.finalize(); | ||||
|  | ||||
|     res.setHeader('Content-Disposition', 'file; filename="' + name + '.tar"'); | ||||
|     res.setHeader('Content-Type', 'application/tar'); | ||||
|  | ||||
|     pack.pipe(res); | ||||
| } | ||||
|  | ||||
| async function exportToMarkdown(branch, res) { | ||||
|     const note = await branch.getNote(); | ||||
|  | ||||
|     if (!await note.hasChildren()) { | ||||
|         await exportSingleMarkdown(note, res); | ||||
|  | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const turndownService = new TurndownService(); | ||||
|     const pack = tar.pack(); | ||||
|     const name = await exportNoteInner(note, ''); | ||||
|  | ||||
|     async function exportNoteInner(note, directory) { | ||||
|         const childFileName = directory + sanitize(note.title); | ||||
|  | ||||
|         if (await note.hasLabel('excludeFromExport')) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         saveDataFile(childFileName, note); | ||||
|  | ||||
|         const childNotes = await note.getChildNotes(); | ||||
|  | ||||
|         if (childNotes.length > 0) { | ||||
|             saveDirectory(childFileName); | ||||
|         } | ||||
|  | ||||
|         for (const childNote of childNotes) { | ||||
|             await exportNoteInner(childNote, childFileName + "/"); | ||||
|         } | ||||
|  | ||||
|         return childFileName; | ||||
|     } | ||||
|  | ||||
|     function saveDataFile(childFileName, note) { | ||||
|         if (note.type !== 'text' && note.type !== 'code') { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (note.content.trim().length === 0) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         let markdown; | ||||
|  | ||||
|         if (note.type === 'code') { | ||||
|             markdown = '```\n' + note.content + "\n```"; | ||||
|         } | ||||
|         else if (note.type === 'text') { | ||||
|             markdown = turndownService.turndown(note.content); | ||||
|         } | ||||
|         else { | ||||
|             // other note types are not supported | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         pack.entry({name: childFileName + ".md", size: markdown.length}, markdown); | ||||
|     } | ||||
|  | ||||
|     function saveDirectory(childFileName) { | ||||
|         pack.entry({name: childFileName, type: 'directory'}); | ||||
|     } | ||||
|  | ||||
|     pack.finalize(); | ||||
|  | ||||
|     res.setHeader('Content-Disposition', 'file; filename="' + name + '.tar"'); | ||||
|     res.setHeader('Content-Type', 'application/tar'); | ||||
|  | ||||
|     pack.pipe(res); | ||||
| } | ||||
|  | ||||
| async function exportSingleMarkdown(note, res) { | ||||
|     const turndownService = new TurndownService(); | ||||
|     const markdown = turndownService.turndown(note.content); | ||||
|     const name = sanitize(note.title); | ||||
|  | ||||
|     res.setHeader('Content-Disposition', 'file; filename="' + name + '.md"'); | ||||
|     res.setHeader('Content-Type', 'text/markdown; charset=UTF-8'); | ||||
|  | ||||
|     res.send(markdown); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     exportNote | ||||
| }; | ||||
| @@ -1,18 +1,11 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const Attribute = require('../../entities/attribute'); | ||||
| const Link = require('../../entities/link'); | ||||
| const repository = require('../../services/repository'); | ||||
| const log = require('../../services/log'); | ||||
| const utils = require('../../services/utils'); | ||||
| const enex = require('../../services/import/enex'); | ||||
| const noteService = require('../../services/notes'); | ||||
| const Branch = require('../../entities/branch'); | ||||
| const tar = require('tar-stream'); | ||||
| const stream = require('stream'); | ||||
| const enexImportService = require('../../services/import/enex'); | ||||
| const opmlImportService = require('../../services/import/opml'); | ||||
| const tarImportService = require('../../services/import/tar'); | ||||
| const markdownImportService = require('../../services/import/markdown'); | ||||
| const path = require('path'); | ||||
| const parseString = require('xml2js').parseString; | ||||
| const commonmark = require('commonmark'); | ||||
|  | ||||
| async function importToBranch(req) { | ||||
|     const parentNoteId = req.params.parentNoteId; | ||||
| @@ -31,338 +24,22 @@ async function importToBranch(req) { | ||||
|     const extension = path.extname(file.originalname).toLowerCase(); | ||||
|  | ||||
|     if (extension === '.tar') { | ||||
|         return await importTar(file, parentNote); | ||||
|         return await tarImportService.importTar(file, parentNote); | ||||
|     } | ||||
|     else if (extension === '.opml') { | ||||
|         return await importOpml(file, parentNote); | ||||
|         return await opmlImportService.importOpml(file, parentNote); | ||||
|     } | ||||
|     else if (extension === '.md') { | ||||
|         return await importMarkdown(file, parentNote); | ||||
|         return await markdownImportService.importMarkdown(file, parentNote); | ||||
|     } | ||||
|     else if (extension === '.enex') { | ||||
|         return await enex.importEnex(file, parentNote); | ||||
|         return await enexImportService.importEnex(file, parentNote); | ||||
|     } | ||||
|     else { | ||||
|         return [400, `Unrecognized extension ${extension}, must be .tar or .opml`]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| function toHtml(text) { | ||||
|     if (!text) { | ||||
|         return ''; | ||||
|     } | ||||
|  | ||||
|     return '<p>' + text.replace(/(?:\r\n|\r|\n)/g, '</p><p>') + '</p>'; | ||||
| } | ||||
|  | ||||
| async function importOutline(outline, parentNoteId) { | ||||
|     const {note} = await noteService.createNote(parentNoteId, outline.$.title, toHtml(outline.$.text)); | ||||
|  | ||||
|     for (const childOutline of (outline.outline || [])) { | ||||
|         await importOutline(childOutline, note.noteId); | ||||
|     } | ||||
|  | ||||
|     return note; | ||||
| } | ||||
|  | ||||
| async function importOpml(file, parentNote) { | ||||
|     const xml = await new Promise(function(resolve, reject) | ||||
|     { | ||||
|         parseString(file.buffer, function (err, result) { | ||||
|             if (err) { | ||||
|                 reject(err); | ||||
|             } | ||||
|             else { | ||||
|                 resolve(result); | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     if (xml.opml.$.version !== '1.0' && xml.opml.$.version !== '1.1') { | ||||
|         return [400, 'Unsupported OPML version ' + xml.opml.$.version + ', 1.0 or 1.1 expected instead.']; | ||||
|     } | ||||
|  | ||||
|     const outlines = xml.opml.body[0].outline || []; | ||||
|     let returnNote = null; | ||||
|  | ||||
|     for (const outline of outlines) { | ||||
|         const note = await importOutline(outline, parentNote.noteId); | ||||
|  | ||||
|         // first created note will be activated after import | ||||
|         returnNote = returnNote || note; | ||||
|     } | ||||
|  | ||||
|     return returnNote; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Complication of this export is the need to balance two needs: | ||||
|  * - | ||||
|  */ | ||||
| async function importTar(file, parentNote) { | ||||
|     const files = await parseImportFile(file); | ||||
|  | ||||
|     const ctx = { | ||||
|         // maps from original noteId (in tar file) to newly generated noteId | ||||
|         noteIdMap: {}, | ||||
|         // new noteIds of notes which were actually created (not just referenced) | ||||
|         createdNoteIds: [], | ||||
|         attributes: [], | ||||
|         links: [], | ||||
|         reader: new commonmark.Parser(), | ||||
|         writer: new commonmark.HtmlRenderer() | ||||
|     }; | ||||
|  | ||||
|     ctx.getNewNoteId = function(origNoteId) { | ||||
|         // in case the original noteId is empty. This probably shouldn't happen, but still good to have this precaution | ||||
|         if (!origNoteId.trim()) { | ||||
|             return ""; | ||||
|         } | ||||
|  | ||||
|         if (!ctx.noteIdMap[origNoteId]) { | ||||
|             ctx.noteIdMap[origNoteId] = utils.newEntityId(); | ||||
|         } | ||||
|  | ||||
|         return ctx.noteIdMap[origNoteId]; | ||||
|     }; | ||||
|  | ||||
|     const note = await importNotes(ctx, files, parentNote.noteId); | ||||
|  | ||||
|     // we save attributes and links after importing notes because we need to check that target noteIds | ||||
|     // have been really created (relation/links with targets outside of the export are not created) | ||||
|  | ||||
|     for (const attr of ctx.attributes) { | ||||
|         if (attr.type === 'relation') { | ||||
|             attr.value = ctx.getNewNoteId(attr.value); | ||||
|  | ||||
|             if (!ctx.createdNoteIds.includes(attr.value)) { | ||||
|                 // relation targets note outside of the export | ||||
|                 continue; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         await new Attribute(attr).save(); | ||||
|     } | ||||
|  | ||||
|     for (const link of ctx.links) { | ||||
|         link.targetNoteId = ctx.getNewNoteId(link.targetNoteId); | ||||
|  | ||||
|         if (!ctx.createdNoteIds.includes(link.targetNoteId)) { | ||||
|             // link targets note outside of the export | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         await new Link(link).save(); | ||||
|     } | ||||
|  | ||||
|     return note; | ||||
| } | ||||
|  | ||||
| function getFileName(name) { | ||||
|     let key; | ||||
|  | ||||
|     if (name.endsWith(".dat")) { | ||||
|         key = "data"; | ||||
|         name = name.substr(0, name.length - 4); | ||||
|     } | ||||
|     else if (name.endsWith(".md")) { | ||||
|         key = "markdown"; | ||||
|         name = name.substr(0, name.length - 3); | ||||
|     } | ||||
|     else if (name.endsWith((".meta"))) { | ||||
|         key = "meta"; | ||||
|         name = name.substr(0, name.length - 5); | ||||
|     } | ||||
|     else { | ||||
|         log.error("Unknown file type in import: " + name); | ||||
|     } | ||||
|  | ||||
|     return {name, key}; | ||||
| } | ||||
|  | ||||
| async function parseImportFile(file) { | ||||
|     const fileMap = {}; | ||||
|     const files = []; | ||||
|  | ||||
|     const extract = tar.extract(); | ||||
|  | ||||
|     extract.on('entry', function(header, stream, next) { | ||||
|         let name, key; | ||||
|  | ||||
|         if (header.type === 'file') { | ||||
|             ({name, key} = getFileName(header.name)); | ||||
|         } | ||||
|         else if (header.type === 'directory') { | ||||
|             // directory entries in tar often end with directory separator | ||||
|             name = (header.name.endsWith("/") || header.name.endsWith("\\")) ? header.name.substr(0, header.name.length - 1) : header.name; | ||||
|             key = 'directory'; | ||||
|         } | ||||
|         else { | ||||
|             log.error("Unrecognized tar entry: " + JSON.stringify(header)); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         let file = fileMap[name]; | ||||
|  | ||||
|         if (!file) { | ||||
|             file = fileMap[name] = { | ||||
|                 name: path.basename(name), | ||||
|                 children: [] | ||||
|             }; | ||||
|  | ||||
|             let parentFileName = path.dirname(header.name); | ||||
|  | ||||
|             if (parentFileName && parentFileName !== '.') { | ||||
|                 fileMap[parentFileName].children.push(file); | ||||
|             } | ||||
|             else { | ||||
|                 files.push(file); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         const chunks = []; | ||||
|  | ||||
|         stream.on("data", function (chunk) { | ||||
|             chunks.push(chunk); | ||||
|         }); | ||||
|  | ||||
|         // header is the tar header | ||||
|         // stream is the content body (might be an empty stream) | ||||
|         // call next when you are done with this entry | ||||
|  | ||||
|         stream.on('end', function() { | ||||
|             file[key] = Buffer.concat(chunks); | ||||
|  | ||||
|             if (key === "meta") { | ||||
|                 file[key] = JSON.parse(file[key].toString("UTF-8")); | ||||
|             } | ||||
|  | ||||
|             next(); // ready for next entry | ||||
|         }); | ||||
|  | ||||
|         stream.resume(); // just auto drain the stream | ||||
|     }); | ||||
|  | ||||
|     return new Promise(resolve => { | ||||
|         extract.on('finish', function() { | ||||
|             resolve(files); | ||||
|         }); | ||||
|  | ||||
|         const bufferStream = new stream.PassThrough(); | ||||
|         bufferStream.end(file.buffer); | ||||
|  | ||||
|         bufferStream.pipe(extract); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| async function importNotes(ctx, files, parentNoteId) { | ||||
|     let returnNote = null; | ||||
|  | ||||
|     for (const file of files) { | ||||
|         let note; | ||||
|  | ||||
|         if (!file.meta) { | ||||
|             let content = ''; | ||||
|  | ||||
|             if (file.data) { | ||||
|                 content = file.data.toString("UTF-8"); | ||||
|             } | ||||
|             else if (file.markdown) { | ||||
|                 const parsed = ctx.reader.parse(file.markdown.toString("UTF-8")); | ||||
|                 content = ctx.writer.render(parsed); | ||||
|             } | ||||
|  | ||||
|             note = (await noteService.createNote(parentNoteId, file.name, content, { | ||||
|                 type: 'text', | ||||
|                 mime: 'text/html' | ||||
|             })).note; | ||||
|         } | ||||
|         else { | ||||
|             if (file.meta.version !== 1) { | ||||
|                 throw new Error("Can't read meta data version " + file.meta.version); | ||||
|             } | ||||
|  | ||||
|             if (file.meta.clone) { | ||||
|                 await new Branch({ | ||||
|                     parentNoteId: parentNoteId, | ||||
|                     noteId: ctx.getNewNoteId(file.meta.noteId), | ||||
|                     prefix: file.meta.prefix, | ||||
|                     isExpanded: !!file.meta.isExpanded | ||||
|                 }).save(); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (file.meta.type !== 'file' && file.meta.type !== 'image') { | ||||
|                 file.data = file.data.toString("UTF-8"); | ||||
|  | ||||
|                 // this will replace all internal links (<a> and <img>) inside the body | ||||
|                 // links pointing outside the export will be broken and changed (ctx.getNewNoteId() will still assign new noteId) | ||||
|                 for (const link of file.meta.links || []) { | ||||
|                     // no need to escape the regexp find string since it's a noteId which doesn't contain any special characters | ||||
|                     file.data = file.data.replace(new RegExp(link.targetNoteId, "g"), ctx.getNewNoteId(link.targetNoteId)); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             note = (await noteService.createNote(parentNoteId, file.meta.title, file.data, { | ||||
|                 noteId: ctx.getNewNoteId(file.meta.noteId), | ||||
|                 type: file.meta.type, | ||||
|                 mime: file.meta.mime, | ||||
|                 prefix: file.meta.prefix | ||||
|             })).note; | ||||
|  | ||||
|             ctx.createdNoteIds.push(note.noteId); | ||||
|  | ||||
|             for (const attribute of file.meta.attributes || []) { | ||||
|                 ctx.attributes.push({ | ||||
|                     noteId: note.noteId, | ||||
|                     type: attribute.type, | ||||
|                     name: attribute.name, | ||||
|                     value: attribute.value, | ||||
|                     isInheritable: attribute.isInheritable, | ||||
|                     position: attribute.position | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             for (const link of file.meta.links || []) { | ||||
|                 ctx.links.push({ | ||||
|                     noteId: note.noteId, | ||||
|                     type: link.type, | ||||
|                     targetNoteId: link.targetNoteId | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // first created note will be activated after import | ||||
|         returnNote = returnNote || note; | ||||
|  | ||||
|         if (file.children.length > 0) { | ||||
|             await importNotes(ctx, file.children, note.noteId); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return returnNote; | ||||
| } | ||||
|  | ||||
| async function importMarkdown(file, parentNote) { | ||||
|     const markdownContent = file.buffer.toString("UTF-8"); | ||||
|  | ||||
|     const reader = new commonmark.Parser(); | ||||
|     const writer = new commonmark.HtmlRenderer(); | ||||
|  | ||||
|     const parsed = reader.parse(markdownContent); | ||||
|     const htmlContent = writer.render(parsed); | ||||
|  | ||||
|     const title = file.originalname.substr(0, file.originalname.length - 3); // strip .md extension | ||||
|  | ||||
|     const {note} = await noteService.createNote(parentNote.noteId, title, htmlContent, { | ||||
|         type: 'text', | ||||
|         mime: 'text/html' | ||||
|     }); | ||||
|  | ||||
|     return note; | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     importToBranch | ||||
| }; | ||||
							
								
								
									
										19
									
								
								src/services/export/markdown_single.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/services/export/markdown_single.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const sanitize = require("sanitize-filename"); | ||||
| const TurndownService = require('turndown'); | ||||
|  | ||||
| async function exportSingleMarkdown(note, res) { | ||||
|     const turndownService = new TurndownService(); | ||||
|     const markdown = turndownService.turndown(note.content); | ||||
|     const name = sanitize(note.title); | ||||
|  | ||||
|     res.setHeader('Content-Disposition', 'file; filename="' + name + '.md"'); | ||||
|     res.setHeader('Content-Type', 'text/markdown; charset=UTF-8'); | ||||
|  | ||||
|     res.send(markdown); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     exportSingleMarkdown | ||||
| }; | ||||
							
								
								
									
										82
									
								
								src/services/export/markdown_tar.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/services/export/markdown_tar.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const tar = require('tar-stream'); | ||||
| const TurndownService = require('turndown'); | ||||
| const sanitize = require("sanitize-filename"); | ||||
| const markdownSingleExportService = require('../../services/export/markdown_single'); | ||||
|  | ||||
| async function exportToMarkdown(branch, res) { | ||||
|     const note = await branch.getNote(); | ||||
|  | ||||
|     if (!await note.hasChildren()) { | ||||
|         await markdownSingleExportService.exportSingleMarkdown(note, res); | ||||
|  | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const turndownService = new TurndownService(); | ||||
|     const pack = tar.pack(); | ||||
|     const name = await exportNoteInner(note, ''); | ||||
|  | ||||
|     async function exportNoteInner(note, directory) { | ||||
|         const childFileName = directory + sanitize(note.title); | ||||
|  | ||||
|         if (await note.hasLabel('excludeFromExport')) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         saveDataFile(childFileName, note); | ||||
|  | ||||
|         const childNotes = await note.getChildNotes(); | ||||
|  | ||||
|         if (childNotes.length > 0) { | ||||
|             saveDirectory(childFileName); | ||||
|         } | ||||
|  | ||||
|         for (const childNote of childNotes) { | ||||
|             await exportNoteInner(childNote, childFileName + "/"); | ||||
|         } | ||||
|  | ||||
|         return childFileName; | ||||
|     } | ||||
|  | ||||
|     function saveDataFile(childFileName, note) { | ||||
|         if (note.type !== 'text' && note.type !== 'code') { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (note.content.trim().length === 0) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         let markdown; | ||||
|  | ||||
|         if (note.type === 'code') { | ||||
|             markdown = '```\n' + note.content + "\n```"; | ||||
|         } | ||||
|         else if (note.type === 'text') { | ||||
|             markdown = turndownService.turndown(note.content); | ||||
|         } | ||||
|         else { | ||||
|             // other note types are not supported | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         pack.entry({name: childFileName + ".md", size: markdown.length}, markdown); | ||||
|     } | ||||
|  | ||||
|     function saveDirectory(childFileName) { | ||||
|         pack.entry({name: childFileName, type: 'directory'}); | ||||
|     } | ||||
|  | ||||
|     pack.finalize(); | ||||
|  | ||||
|     res.setHeader('Content-Disposition', 'file; filename="' + name + '.tar"'); | ||||
|     res.setHeader('Content-Type', 'application/tar'); | ||||
|  | ||||
|     pack.pipe(res); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     exportToMarkdown | ||||
| }; | ||||
							
								
								
									
										103
									
								
								src/services/export/native_tar.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								src/services/export/native_tar.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const html = require('html'); | ||||
| const native_tar = require('tar-stream'); | ||||
| const sanitize = require("sanitize-filename"); | ||||
|  | ||||
| async function exportToTar(branch, res) { | ||||
|     const pack = native_tar.pack(); | ||||
|  | ||||
|     const exportedNoteIds = []; | ||||
|     const name = await exportNoteInner(branch, ''); | ||||
|  | ||||
|     async function exportNoteInner(branch, directory) { | ||||
|         const note = await branch.getNote(); | ||||
|         const childFileName = directory + sanitize(note.title); | ||||
|  | ||||
|         if (exportedNoteIds.includes(note.noteId)) { | ||||
|             saveMetadataFile(childFileName, { | ||||
|                 version: 1, | ||||
|                 clone: true, | ||||
|                 noteId: note.noteId, | ||||
|                 prefix: branch.prefix | ||||
|             }); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const metadata = { | ||||
|             version: 1, | ||||
|             clone: false, | ||||
|             noteId: note.noteId, | ||||
|             title: note.title, | ||||
|             prefix: branch.prefix, | ||||
|             isExpanded: branch.isExpanded, | ||||
|             type: note.type, | ||||
|             mime: note.mime, | ||||
|             // we don't export dateCreated and dateModified of any entity since that would be a bit misleading | ||||
|             attributes: (await note.getOwnedAttributes()).map(attribute => { | ||||
|                 return { | ||||
|                     type: attribute.type, | ||||
|                     name: attribute.name, | ||||
|                     value: attribute.value, | ||||
|                     isInheritable: attribute.isInheritable, | ||||
|                     position: attribute.position | ||||
|                 }; | ||||
|             }), | ||||
|             links: (await note.getLinks()).map(link => { | ||||
|                 return { | ||||
|                     type: link.type, | ||||
|                     targetNoteId: link.targetNoteId | ||||
|                 } | ||||
|             }) | ||||
|         }; | ||||
|  | ||||
|         if (await note.hasLabel('excludeFromExport')) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         saveMetadataFile(childFileName, metadata); | ||||
|         saveDataFile(childFileName, note); | ||||
|  | ||||
|         exportedNoteIds.push(note.noteId); | ||||
|  | ||||
|         const childBranches = await note.getChildBranches(); | ||||
|  | ||||
|         if (childBranches.length > 0) { | ||||
|             saveDirectory(childFileName); | ||||
|         } | ||||
|  | ||||
|         for (const childBranch of childBranches) { | ||||
|             await exportNoteInner(childBranch, childFileName + "/"); | ||||
|         } | ||||
|  | ||||
|         return childFileName; | ||||
|     } | ||||
|  | ||||
|     function saveDataFile(childFileName, note) { | ||||
|         const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content; | ||||
|  | ||||
|         pack.entry({name: childFileName + ".dat", size: content.length}, content); | ||||
|     } | ||||
|  | ||||
|     function saveMetadataFile(childFileName, metadata) { | ||||
|         const metadataJson = JSON.stringify(metadata, null, '\t'); | ||||
|  | ||||
|         pack.entry({name: childFileName + ".meta", size: metadataJson.length}, metadataJson); | ||||
|     } | ||||
|  | ||||
|     function saveDirectory(childFileName) { | ||||
|         pack.entry({name: childFileName, type: 'directory'}); | ||||
|     } | ||||
|  | ||||
|     pack.finalize(); | ||||
|  | ||||
|     res.setHeader('Content-Disposition', 'file; filename="' + name + '.tar"'); | ||||
|     res.setHeader('Content-Type', 'application/tar'); | ||||
|  | ||||
|     pack.pipe(res); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     exportToTar | ||||
| }; | ||||
							
								
								
									
										67
									
								
								src/services/export/opml.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/services/export/opml.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const sanitize = require("sanitize-filename"); | ||||
| const repository = require("../../services/repository"); | ||||
| const utils = require('../../services/utils'); | ||||
|  | ||||
| async function exportToOpml(branch, res) { | ||||
|     const note = await branch.getNote(); | ||||
|     const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title; | ||||
|     const sanitizedTitle = sanitize(title); | ||||
|  | ||||
|     async function exportNoteInner(branchId) { | ||||
|         const branch = await repository.getBranch(branchId); | ||||
|         const note = await branch.getNote(); | ||||
|         const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title; | ||||
|  | ||||
|         const preparedTitle = prepareText(title); | ||||
|         const preparedContent = prepareText(note.content); | ||||
|  | ||||
|         res.write(`<outline title="${preparedTitle}" text="${preparedContent}">\n`); | ||||
|  | ||||
|         for (const child of await note.getChildBranches()) { | ||||
|             await exportNoteInner(child.branchId); | ||||
|         } | ||||
|  | ||||
|         res.write('</outline>'); | ||||
|     } | ||||
|  | ||||
|     res.setHeader('Content-Disposition', 'file; filename="' + sanitizedTitle + '.opml"'); | ||||
|     res.setHeader('Content-Type', 'text/x-opml'); | ||||
|  | ||||
|     res.write(`<?xml version="1.0" encoding="UTF-8"?> | ||||
| <opml version="1.0"> | ||||
| <head> | ||||
| <title>Trilium export</title> | ||||
| </head> | ||||
| <body>`); | ||||
|  | ||||
|     await exportNoteInner(branch.branchId); | ||||
|  | ||||
|     res.write(`</body> | ||||
| </opml>`); | ||||
|     res.end(); | ||||
| } | ||||
|  | ||||
| function prepareText(text) { | ||||
|     const newLines = text.replace(/(<p[^>]*>|<br\s*\/?>)/g, '\n') | ||||
|         .replace(/ /g, ' '); // nbsp isn't in XML standard (only HTML) | ||||
|  | ||||
|     const stripped = utils.stripTags(newLines); | ||||
|  | ||||
|     const escaped = escapeXmlAttribute(stripped); | ||||
|  | ||||
|     return escaped.replace(/\n/g, '
'); | ||||
| } | ||||
|  | ||||
| function escapeXmlAttribute(text) { | ||||
|     return text.replace(/&/g, '&') | ||||
|         .replace(/</g, '<') | ||||
|         .replace(/>/g, '>') | ||||
|         .replace(/"/g, '"') | ||||
|         .replace(/'/g, '''); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     exportToOpml | ||||
| }; | ||||
							
								
								
									
										30
									
								
								src/services/import/markdown.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/services/import/markdown.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| "use strict"; | ||||
|  | ||||
| // note that this is for import of single markdown file only - for archive/structure of markdown files | ||||
| // see tar export/import | ||||
|  | ||||
| const noteService = require('../../services/notes'); | ||||
| const commonmark = require('commonmark'); | ||||
|  | ||||
| async function importMarkdown(file, parentNote) { | ||||
|     const markdownContent = file.buffer.toString("UTF-8"); | ||||
|  | ||||
|     const reader = new commonmark.Parser(); | ||||
|     const writer = new commonmark.HtmlRenderer(); | ||||
|  | ||||
|     const parsed = reader.parse(markdownContent); | ||||
|     const htmlContent = writer.render(parsed); | ||||
|  | ||||
|     const title = file.originalname.substr(0, file.originalname.length - 3); // strip .md extension | ||||
|  | ||||
|     const {note} = await noteService.createNote(parentNote.noteId, title, htmlContent, { | ||||
|         type: 'text', | ||||
|         mime: 'text/html' | ||||
|     }); | ||||
|  | ||||
|     return note; | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     importMarkdown | ||||
| }; | ||||
							
								
								
									
										56
									
								
								src/services/import/opml.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/services/import/opml.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const noteService = require('../../services/notes'); | ||||
| const parseString = require('xml2js').parseString; | ||||
|  | ||||
| async function importOpml(file, parentNote) { | ||||
|     const xml = await new Promise(function(resolve, reject) | ||||
|     { | ||||
|         parseString(file.buffer, function (err, result) { | ||||
|             if (err) { | ||||
|                 reject(err); | ||||
|             } | ||||
|             else { | ||||
|                 resolve(result); | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     if (xml.opml.$.version !== '1.0' && xml.opml.$.version !== '1.1') { | ||||
|         return [400, 'Unsupported OPML version ' + xml.opml.$.version + ', 1.0 or 1.1 expected instead.']; | ||||
|     } | ||||
|  | ||||
|     const outlines = xml.opml.body[0].outline || []; | ||||
|     let returnNote = null; | ||||
|  | ||||
|     for (const outline of outlines) { | ||||
|         const note = await importOutline(outline, parentNote.noteId); | ||||
|  | ||||
|         // first created note will be activated after import | ||||
|         returnNote = returnNote || note; | ||||
|     } | ||||
|  | ||||
|     return returnNote; | ||||
| } | ||||
|  | ||||
| function toHtml(text) { | ||||
|     if (!text) { | ||||
|         return ''; | ||||
|     } | ||||
|  | ||||
|     return '<p>' + text.replace(/(?:\r\n|\r|\n)/g, '</p><p>') + '</p>'; | ||||
| } | ||||
|  | ||||
| async function importOutline(outline, parentNoteId) { | ||||
|     const {note} = await noteService.createNote(parentNoteId, outline.$.title, toHtml(outline.$.text)); | ||||
|  | ||||
|     for (const childOutline of (outline.outline || [])) { | ||||
|         await importOutline(childOutline, note.noteId); | ||||
|     } | ||||
|  | ||||
|     return note; | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     importOpml | ||||
| }; | ||||
							
								
								
									
										265
									
								
								src/services/import/tar.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										265
									
								
								src/services/import/tar.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,265 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const Attribute = require('../../entities/attribute'); | ||||
| const Link = require('../../entities/link'); | ||||
| const log = require('../../services/log'); | ||||
| const utils = require('../../services/utils'); | ||||
| const noteService = require('../../services/notes'); | ||||
| const Branch = require('../../entities/branch'); | ||||
| const tar = require('tar-stream'); | ||||
| const stream = require('stream'); | ||||
| const path = require('path'); | ||||
| const commonmark = require('commonmark'); | ||||
|  | ||||
| /** | ||||
|  * Complication of this export is the need to balance two needs: | ||||
|  * - | ||||
|  */ | ||||
| async function importTar(file, parentNote) { | ||||
|     const files = await parseImportFile(file); | ||||
|  | ||||
|     const ctx = { | ||||
|         // maps from original noteId (in tar file) to newly generated noteId | ||||
|         noteIdMap: {}, | ||||
|         // new noteIds of notes which were actually created (not just referenced) | ||||
|         createdNoteIds: [], | ||||
|         attributes: [], | ||||
|         links: [], | ||||
|         reader: new commonmark.Parser(), | ||||
|         writer: new commonmark.HtmlRenderer() | ||||
|     }; | ||||
|  | ||||
|     ctx.getNewNoteId = function(origNoteId) { | ||||
|         // in case the original noteId is empty. This probably shouldn't happen, but still good to have this precaution | ||||
|         if (!origNoteId.trim()) { | ||||
|             return ""; | ||||
|         } | ||||
|  | ||||
|         if (!ctx.noteIdMap[origNoteId]) { | ||||
|             ctx.noteIdMap[origNoteId] = utils.newEntityId(); | ||||
|         } | ||||
|  | ||||
|         return ctx.noteIdMap[origNoteId]; | ||||
|     }; | ||||
|  | ||||
|     const note = await importNotes(ctx, files, parentNote.noteId); | ||||
|  | ||||
|     // we save attributes and links after importing notes because we need to check that target noteIds | ||||
|     // have been really created (relation/links with targets outside of the export are not created) | ||||
|  | ||||
|     for (const attr of ctx.attributes) { | ||||
|         if (attr.type === 'relation') { | ||||
|             attr.value = ctx.getNewNoteId(attr.value); | ||||
|  | ||||
|             if (!ctx.createdNoteIds.includes(attr.value)) { | ||||
|                 // relation targets note outside of the export | ||||
|                 continue; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         await new Attribute(attr).save(); | ||||
|     } | ||||
|  | ||||
|     for (const link of ctx.links) { | ||||
|         link.targetNoteId = ctx.getNewNoteId(link.targetNoteId); | ||||
|  | ||||
|         if (!ctx.createdNoteIds.includes(link.targetNoteId)) { | ||||
|             // link targets note outside of the export | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         await new Link(link).save(); | ||||
|     } | ||||
|  | ||||
|     return note; | ||||
| } | ||||
|  | ||||
| function getFileName(name) { | ||||
|     let key; | ||||
|  | ||||
|     if (name.endsWith(".dat")) { | ||||
|         key = "data"; | ||||
|         name = name.substr(0, name.length - 4); | ||||
|     } | ||||
|     else if (name.endsWith(".md")) { | ||||
|         key = "markdown"; | ||||
|         name = name.substr(0, name.length - 3); | ||||
|     } | ||||
|     else if (name.endsWith((".meta"))) { | ||||
|         key = "meta"; | ||||
|         name = name.substr(0, name.length - 5); | ||||
|     } | ||||
|     else { | ||||
|         log.error("Unknown file type in import: " + name); | ||||
|     } | ||||
|  | ||||
|     return {name, key}; | ||||
| } | ||||
|  | ||||
| async function parseImportFile(file) { | ||||
|     const fileMap = {}; | ||||
|     const files = []; | ||||
|  | ||||
|     const extract = tar.extract(); | ||||
|  | ||||
|     extract.on('entry', function(header, stream, next) { | ||||
|         let name, key; | ||||
|  | ||||
|         if (header.type === 'file') { | ||||
|             ({name, key} = getFileName(header.name)); | ||||
|         } | ||||
|         else if (header.type === 'directory') { | ||||
|             // directory entries in tar often end with directory separator | ||||
|             name = (header.name.endsWith("/") || header.name.endsWith("\\")) ? header.name.substr(0, header.name.length - 1) : header.name; | ||||
|             key = 'directory'; | ||||
|         } | ||||
|         else { | ||||
|             log.error("Unrecognized tar entry: " + JSON.stringify(header)); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         let file = fileMap[name]; | ||||
|  | ||||
|         if (!file) { | ||||
|             file = fileMap[name] = { | ||||
|                 name: path.basename(name), | ||||
|                 children: [] | ||||
|             }; | ||||
|  | ||||
|             let parentFileName = path.dirname(header.name); | ||||
|  | ||||
|             if (parentFileName && parentFileName !== '.') { | ||||
|                 fileMap[parentFileName].children.push(file); | ||||
|             } | ||||
|             else { | ||||
|                 files.push(file); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         const chunks = []; | ||||
|  | ||||
|         stream.on("data", function (chunk) { | ||||
|             chunks.push(chunk); | ||||
|         }); | ||||
|  | ||||
|         // header is the tar header | ||||
|         // stream is the content body (might be an empty stream) | ||||
|         // call next when you are done with this entry | ||||
|  | ||||
|         stream.on('end', function() { | ||||
|             file[key] = Buffer.concat(chunks); | ||||
|  | ||||
|             if (key === "meta") { | ||||
|                 file[key] = JSON.parse(file[key].toString("UTF-8")); | ||||
|             } | ||||
|  | ||||
|             next(); // ready for next entry | ||||
|         }); | ||||
|  | ||||
|         stream.resume(); // just auto drain the stream | ||||
|     }); | ||||
|  | ||||
|     return new Promise(resolve => { | ||||
|         extract.on('finish', function() { | ||||
|             resolve(files); | ||||
|         }); | ||||
|  | ||||
|         const bufferStream = new stream.PassThrough(); | ||||
|         bufferStream.end(file.buffer); | ||||
|  | ||||
|         bufferStream.pipe(extract); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| async function importNotes(ctx, files, parentNoteId) { | ||||
|     let returnNote = null; | ||||
|  | ||||
|     for (const file of files) { | ||||
|         let note; | ||||
|  | ||||
|         if (!file.meta) { | ||||
|             let content = ''; | ||||
|  | ||||
|             if (file.data) { | ||||
|                 content = file.data.toString("UTF-8"); | ||||
|             } | ||||
|             else if (file.markdown) { | ||||
|                 const parsed = ctx.reader.parse(file.markdown.toString("UTF-8")); | ||||
|                 content = ctx.writer.render(parsed); | ||||
|             } | ||||
|  | ||||
|             note = (await noteService.createNote(parentNoteId, file.name, content, { | ||||
|                 type: 'text', | ||||
|                 mime: 'text/html' | ||||
|             })).note; | ||||
|         } | ||||
|         else { | ||||
|             if (file.meta.version !== 1) { | ||||
|                 throw new Error("Can't read meta data version " + file.meta.version); | ||||
|             } | ||||
|  | ||||
|             if (file.meta.clone) { | ||||
|                 await new Branch({ | ||||
|                     parentNoteId: parentNoteId, | ||||
|                     noteId: ctx.getNewNoteId(file.meta.noteId), | ||||
|                     prefix: file.meta.prefix, | ||||
|                     isExpanded: !!file.meta.isExpanded | ||||
|                 }).save(); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (file.meta.type !== 'file' && file.meta.type !== 'image') { | ||||
|                 file.data = file.data.toString("UTF-8"); | ||||
|  | ||||
|                 // this will replace all internal links (<a> and <img>) inside the body | ||||
|                 // links pointing outside the export will be broken and changed (ctx.getNewNoteId() will still assign new noteId) | ||||
|                 for (const link of file.meta.links || []) { | ||||
|                     // no need to escape the regexp find string since it's a noteId which doesn't contain any special characters | ||||
|                     file.data = file.data.replace(new RegExp(link.targetNoteId, "g"), ctx.getNewNoteId(link.targetNoteId)); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             note = (await noteService.createNote(parentNoteId, file.meta.title, file.data, { | ||||
|                 noteId: ctx.getNewNoteId(file.meta.noteId), | ||||
|                 type: file.meta.type, | ||||
|                 mime: file.meta.mime, | ||||
|                 prefix: file.meta.prefix | ||||
|             })).note; | ||||
|  | ||||
|             ctx.createdNoteIds.push(note.noteId); | ||||
|  | ||||
|             for (const attribute of file.meta.attributes || []) { | ||||
|                 ctx.attributes.push({ | ||||
|                     noteId: note.noteId, | ||||
|                     type: attribute.type, | ||||
|                     name: attribute.name, | ||||
|                     value: attribute.value, | ||||
|                     isInheritable: attribute.isInheritable, | ||||
|                     position: attribute.position | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             for (const link of file.meta.links || []) { | ||||
|                 ctx.links.push({ | ||||
|                     noteId: note.noteId, | ||||
|                     type: link.type, | ||||
|                     targetNoteId: link.targetNoteId | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // first created note will be activated after import | ||||
|         returnNote = returnNote || note; | ||||
|  | ||||
|         if (file.children.length > 0) { | ||||
|             await importNotes(ctx, file.children, note.noteId); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return returnNote; | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     importTar | ||||
| }; | ||||
| @@ -14,7 +14,7 @@ | ||||
|                     <br/> | ||||
|  | ||||
|                     <div class="form-check"> | ||||
|                         <input class="form-check-input" type="radio" name="export-format" id="export-format-tar" value="tar" checked> | ||||
|                         <input class="form-check-input" type="radio" name="export-format" id="export-format-tar" value="native-tar" checked> | ||||
|                         <label class="form-check-label" for="export-format-tar">Native TAR - this is Trilium's native format which preserves all notes' data & metadata.</label> | ||||
|                     </div> | ||||
|  | ||||
| @@ -31,7 +31,7 @@ | ||||
|  | ||||
|                     <div class="form-check disabled"> | ||||
|                         <input class="form-check-input" type="radio" name="export-format" id="export-format-markdown" | ||||
|                                value="markdown"> | ||||
|                                value="markdown-tar"> | ||||
|                         <label class="form-check-label" for="export-format-markdown"> | ||||
|                             Markdown - TAR archive of Markdown formatted notes | ||||
|                         </label> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user