From 5b85713bf339bffdce55e16f8bdc2d1e741253ce Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 11 Sep 2023 00:12:12 +0200 Subject: [PATCH] saving export meta file per directory --- src/services/export/zip.js | 135 +++++++++++++++++++++------------ src/services/meta/meta_file.js | 16 ++++ src/services/meta/note_meta.js | 10 ++- 3 files changed, 111 insertions(+), 50 deletions(-) create mode 100644 src/services/meta/meta_file.js diff --git a/src/services/export/zip.js b/src/services/export/zip.js index 704b94b6f..c21546672 100644 --- a/src/services/export/zip.js +++ b/src/services/export/zip.js @@ -16,6 +16,7 @@ const archiver = require('archiver'); const log = require("../log"); const TaskContext = require("../task_context"); const ValidationError = require("../../errors/validation_error"); +const MetaFile = require("../meta/meta_file"); const NoteMeta = require("../meta/note_meta"); const AttachmentMeta = require("../meta/attachment_meta"); const AttributeMeta = require("../meta/attribute_meta"); @@ -39,6 +40,24 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) /** @type {Object.} */ const noteIdToMeta = {}; + /** @type {Object.} */ + const childNoteIdToParentMeta = {}; + + /** @type {Object.} */ + const parentNoteIdToChildrenMeta = {}; + + function getNotePath(noteId) { + const notePath = [noteId]; + + let cur = noteIdToMeta[noteId]; + while (cur = childNoteIdToParentMeta[cur.noteId]) { + notePath.push(cur.noteId); + } + + notePath.reverse(); + return notePath; + } + /** * @param {Object.} existingFileNames * @param {string} fileName @@ -116,13 +135,49 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) return getUniqueFilename(existingFileNames, fileName); } + /** + * @param {BBranch[]} branches + * @param {Object.} existingFileNames + * @returns {MetaFile} + */ + function createMetaFileForDirectory(branches, existingFileNames = {}) { + // namespace is shared by children in the same note + const metas = []; + + for (const branch of branches) { + const noteMeta = createNoteMeta(branch, existingFileNames); + + // can be undefined if export is disabled for this note + if (noteMeta) { + metas.push(noteMeta); + + const parentMeta = noteIdToMeta[branch.parentNoteId]; + + if (parentMeta) { + childNoteIdToParentMeta[noteMeta.noteId] = parentMeta; + parentNoteIdToChildrenMeta[parentMeta.noteId] = parentNoteIdToChildrenMeta[parentMeta.noteId] || []; + parentNoteIdToChildrenMeta[parentMeta.noteId].push(noteMeta); + } + } + } + + if (!metas.length) { + return null; + } + + const metaFile = new MetaFile(); + metaFile.formatVersion = 3; + metaFile.appVersion = packageInfo.version; + metaFile.files = metas; + return metaFile; + } + /** * @param {BBranch} branch - * @param {NoteMeta} parentMeta * @param {Object.} existingFileNames * @returns {NoteMeta|null} */ - function createNoteMeta(branch, parentMeta, existingFileNames) { + function createNoteMeta(branch, existingFileNames) { const note = branch.getNote(); if (note.hasOwnedLabel('excludeFromExport')) { @@ -137,15 +192,13 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) baseFileName = baseFileName.substr(0, 200); } - const notePath = parentMeta.notePath.concat([note.noteId]); - if (note.noteId in noteIdToMeta) { - const fileName = getUniqueFilename(existingFileNames, `${baseFileName}.clone.${format === 'html' ? 'html' : 'md'}`); + const fileName = getUniqueFilename(existingFileNames, + `${baseFileName}.clone.${format === 'html' ? 'html' : 'md'}`); const meta = new NoteMeta(); meta.isClone = true; meta.noteId = note.noteId; - meta.notePath = notePath; meta.title = note.getTitleOrProtected(); meta.prefix = branch.prefix; meta.dataFileName = fileName; @@ -157,7 +210,6 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) const meta = new NoteMeta(); meta.isClone = false; meta.noteId = note.noteId; - meta.notePath = notePath; meta.title = note.getTitleOrProtected(); meta.notePosition = branch.notePosition; meta.prefix = branch.prefix; @@ -212,19 +264,6 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) if (childBranches.length > 0) { meta.dirFileName = getUniqueFilename(existingFileNames, baseFileName); - meta.children = []; - - // namespace is shared by children in the same note - const childExistingNames = {}; - - for (const childBranch of childBranches) { - const note = createNoteMeta(childBranch, meta, childExistingNames); - - // can be undefined if export is disabled for this note - if (note) { - meta.children.push(note); - } - } } return meta; @@ -242,8 +281,8 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) return null; } - const targetPath = targetMeta.notePath.slice(); - const sourcePath = sourceMeta.notePath.slice(); + const targetPath = getNotePath(targetMeta.noteId); + const sourcePath = getNotePath(sourceMeta.noteId); // > 1 for the edge case that targetPath and sourcePath are exact same (a link to itself) while (targetPath.length > 1 && sourcePath.length > 1 && targetPath[0] === sourcePath[0]) { @@ -328,7 +367,7 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) if (noteMeta.format === 'html') { if (!content.substr(0, 100).toLowerCase().includes(" element will make sure external links are openable - https://github.com/zadam/trilium/issues/1289#issuecomment-704066809 @@ -409,14 +448,17 @@ ${markdownContent}`; }); } - if (noteMeta.children?.length > 0) { - const directoryPath = filePathPrefix + noteMeta.dirFileName; + const metaFile = createMetaFileForDirectory(note.getChildBranches()); + if (metaFile) { + const childFilePathPrefix = `${filePathPrefix}${noteMeta.dirFileName}/`; // create directory - archive.append('', { name: `${directoryPath}/`, date: dateUtils.parseDateTime(note.utcDateModified) }); + archive.append('', { name: childFilePathPrefix, date: dateUtils.parseDateTime(note.utcDateModified) }); - for (const childMeta of noteMeta.children) { - saveNote(childMeta, `${directoryPath}/`); + metaFile.save(archive, childFilePathPrefix); + + for (const childMeta of metaFile.files) { + saveNote(childMeta, childFilePathPrefix); } } } @@ -440,11 +482,13 @@ ${markdownContent}`; html += escapedTitle; } - if (meta.children && meta.children.length > 0) { + const childrenMeta = parentNoteIdToChildrenMeta[meta.noteId]; + + if (childrenMeta?.length > 0) { html += '
    '; - for (const child of meta.children) { - html += saveNavigationInner(child); + for (const childMeta of childrenMeta) { + html += saveNavigationInner(childMeta); } html += '
' @@ -482,8 +526,10 @@ ${markdownContent}`; firstNonEmptyNote = getNoteTargetUrl(curMeta.noteId, rootMeta); } - if (curMeta.children && curMeta.children.length > 0) { - curMeta = curMeta.children[0]; + const childrenMeta = parentNoteIdToChildrenMeta[curMeta.noteId]; + + if (childrenMeta?.length > 0) { + curMeta = childrenMeta[0]; } else { break; @@ -515,14 +561,13 @@ ${markdownContent}`; archive.append(cssContent, { name: cssMeta.dataFileName }); } - const existingFileNames = format === 'html' ? ['navigation', 'index'] : []; - const rootMeta = createNoteMeta(branch, { notePath: [] }, existingFileNames); + const existingFileNames = format === 'html' ? { 'navigation': 1, 'index': 1 } : {}; + const metaFile = createMetaFileForDirectory([branch], existingFileNames); - const metaFile = { - formatVersion: 2, - appVersion: packageInfo.version, - files: [ rootMeta ] - }; + if (!metaFile) { // corner case of disabled export for exported note + res.sendStatus(400); + return; + } let navigationMeta, indexMeta, cssMeta; @@ -565,15 +610,9 @@ ${markdownContent}`; }); } - if (!rootMeta) { // corner case of disabled export for exported note - res.sendStatus(400); - return; - } - - const metaFileJson = JSON.stringify(metaFile, null, '\t'); - - archive.append(metaFileJson, { name: "!!!meta.json" }); + metaFile.save(archive); + const rootMeta = metaFile.files[0]; saveNote(rootMeta, ''); if (format === 'html') { diff --git a/src/services/meta/meta_file.js b/src/services/meta/meta_file.js new file mode 100644 index 000000000..9fcdff480 --- /dev/null +++ b/src/services/meta/meta_file.js @@ -0,0 +1,16 @@ +class MetaFile { + /** @type {int} */ + formatVersion; + /** @type {string} */ + appVersion; + /** @type {NoteMeta[]} */ + files; + + save(archive, filePathPrefix = '') { + const metaFileJson = JSON.stringify(this, null, '\t'); + + archive.append(metaFileJson, { name: filePathPrefix + "!!!meta.json" }); + } +} + +module.exports = MetaFile; diff --git a/src/services/meta/note_meta.js b/src/services/meta/note_meta.js index fd24381d6..3c280d978 100644 --- a/src/services/meta/note_meta.js +++ b/src/services/meta/note_meta.js @@ -1,7 +1,10 @@ class NoteMeta { /** @type {string} */ noteId; - /** @type {string} */ + /** + * @type {string[]} + * @deprecated unused as of v3 + */ notePath; /** @type {boolean} */ isClone; @@ -29,7 +32,10 @@ class NoteMeta { attributes; /** @type {AttachmentMeta[]} */ attachments; - /** @type {NoteMeta[]|undefined} */ + /** + * @type {NoteMeta[]|undefined} + * @deprecated unused as of v3, there's meta file for each directory, avoiding the need to use this + */ children; }