From f6944b821972ee6be9f8e427bcd9d4f80f780e03 Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 6 May 2023 14:38:45 +0200 Subject: [PATCH] introducing meta types for import/export, fixes for attachments export --- src/becca/entities/battachment.js | 2 +- src/becca/entities/battribute.js | 2 +- src/becca/entities/bblob.js | 4 +- src/becca/entities/bbranch.js | 2 +- src/becca/entities/bnote_revision.js | 2 +- src/public/app/entities/fattribute.js | 2 +- src/public/app/entities/fbranch.js | 2 +- .../app/services/frontend_script_api.js | 2 +- src/routes/api/note_map.js | 2 +- src/services/backend_script_api.js | 4 +- src/services/export/zip.js | 168 +++++++++++------- src/services/meta/attachment_meta.js | 14 ++ src/services/meta/attribute_meta.js | 14 ++ src/services/meta/note_meta.js | 34 ++++ src/services/options.js | 2 +- src/share/shaca/entities/sattribute.js | 2 +- 16 files changed, 183 insertions(+), 75 deletions(-) create mode 100644 src/services/meta/attachment_meta.js create mode 100644 src/services/meta/attribute_meta.js create mode 100644 src/services/meta/note_meta.js diff --git a/src/becca/entities/battachment.js b/src/becca/entities/battachment.js index 79a23b0fb..c106f375c 100644 --- a/src/becca/entities/battachment.js +++ b/src/becca/entities/battachment.js @@ -45,7 +45,7 @@ class BAttachment extends AbstractBeccaEntity { this.mime = row.mime; /** @type {string} */ this.title = row.title; - /** @type {number} */ + /** @type {integer} */ this.position = row.position; /** @type {string} */ this.blobId = row.blobId; diff --git a/src/becca/entities/battribute.js b/src/becca/entities/battribute.js index e57691e94..a203d4469 100644 --- a/src/becca/entities/battribute.js +++ b/src/becca/entities/battribute.js @@ -51,7 +51,7 @@ class BAttribute extends AbstractBeccaEntity { this.type = type; /** @type {string} */ this.name = name; - /** @type {int} */ + /** @type {integer} */ this.position = position; /** @type {string} */ this.value = value || ""; diff --git a/src/becca/entities/bblob.js b/src/becca/entities/bblob.js index 2af26cb40..bb8c18616 100644 --- a/src/becca/entities/bblob.js +++ b/src/becca/entities/bblob.js @@ -4,7 +4,7 @@ class BBlob { this.blobId = row.blobId; /** @type {string|Buffer} */ this.content = row.content; - /** @type {number} */ + /** @type {integer} */ this.contentLength = row.contentLength; /** @type {string} */ this.dateModified = row.dateModified; @@ -23,4 +23,4 @@ class BBlob { } } -module.exports = BBlob; \ No newline at end of file +module.exports = BBlob; diff --git a/src/becca/entities/bbranch.js b/src/becca/entities/bbranch.js index 43574fd46..456d1c289 100644 --- a/src/becca/entities/bbranch.js +++ b/src/becca/entities/bbranch.js @@ -55,7 +55,7 @@ class BBranch extends AbstractBeccaEntity { this.parentNoteId = parentNoteId; /** @type {string|null} */ this.prefix = prefix; - /** @type {int} */ + /** @type {integer} */ this.notePosition = notePosition; /** @type {boolean} */ this.isExpanded = !!isExpanded; diff --git a/src/becca/entities/bnote_revision.js b/src/becca/entities/bnote_revision.js index 190b0fa9a..3a0a0f888 100644 --- a/src/becca/entities/bnote_revision.js +++ b/src/becca/entities/bnote_revision.js @@ -46,7 +46,7 @@ class BNoteRevision extends AbstractBeccaEntity { this.utcDateCreated = row.utcDateCreated; /** @type {string} */ this.utcDateModified = row.utcDateModified; - /** @type {number} */ + /** @type {integer} */ this.contentLength = row.contentLength; if (this.isProtected && !titleDecrypted) { diff --git a/src/public/app/entities/fattribute.js b/src/public/app/entities/fattribute.js index ce166e038..bfc488f6d 100644 --- a/src/public/app/entities/fattribute.js +++ b/src/public/app/entities/fattribute.js @@ -22,7 +22,7 @@ class FAttribute { this.name = row.name; /** @type {string} */ this.value = row.value; - /** @type {int} */ + /** @type {integer} */ this.position = row.position; /** @type {boolean} */ this.isInheritable = !!row.isInheritable; diff --git a/src/public/app/entities/fbranch.js b/src/public/app/entities/fbranch.js index b1746c361..805d27f17 100644 --- a/src/public/app/entities/fbranch.js +++ b/src/public/app/entities/fbranch.js @@ -19,7 +19,7 @@ class FBranch { this.noteId = row.noteId; /** @type {string} */ this.parentNoteId = row.parentNoteId; - /** @type {int} */ + /** @type {integer} */ this.notePosition = row.notePosition; /** @type {string} */ this.prefix = row.prefix; diff --git a/src/public/app/services/frontend_script_api.js b/src/public/app/services/frontend_script_api.js index 91b75a626..24e83f484 100644 --- a/src/public/app/services/frontend_script_api.js +++ b/src/public/app/services/frontend_script_api.js @@ -478,7 +478,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain * Return randomly generated string of given length. This random string generation is NOT cryptographically secure. * * @method - * @param {number} length of the string + * @param {integer} length of the string * @returns {string} random string */ this.randomString = utils.randomString; diff --git a/src/routes/api/note_map.js b/src/routes/api/note_map.js index 11fc9a3e6..b2725aa66 100644 --- a/src/routes/api/note_map.js +++ b/src/routes/api/note_map.js @@ -35,7 +35,7 @@ function buildDescendantCountMap(noteIdsToCount) { } /** * @param {BNote} note - * @param {int} depth + * @param {integer} depth * @returns {string[]} noteIds */ function getNeighbors(note, depth) { diff --git a/src/services/backend_script_api.js b/src/services/backend_script_api.js index d2906df91..17f64296b 100644 --- a/src/services/backend_script_api.js +++ b/src/services/backend_script_api.js @@ -215,7 +215,7 @@ function BackendScriptApi(currentNote, apiParams) { * @property {boolean} [params.isProtected=false] * @property {boolean} [params.isExpanded=false] * @property {string} [params.prefix=''] - * @property {int} [params.notePosition] - default is last existing notePosition in a parent + 10 + * @property {integer} [params.notePosition] - default is last existing notePosition in a parent + 10 * @returns {{note: BNote, branch: BBranch}} object contains newly created entities note and branch */ this.createNewNote = noteService.createNewNote; @@ -412,7 +412,7 @@ function BackendScriptApi(currentNote, apiParams) { * Return randomly generated string of given length. This random string generation is NOT cryptographically secure. * * @method - * @param {number} length of the string + * @param {integer} length of the string * @returns {string} random string */ this.randomString = utils.randomString; diff --git a/src/services/export/zip.js b/src/services/export/zip.js index 7acacb864..f56a7c741 100644 --- a/src/services/export/zip.js +++ b/src/services/export/zip.js @@ -16,6 +16,9 @@ const archiver = require('archiver'); const log = require("../log"); const TaskContext = require("../task_context"); const ValidationError = require("../../errors/validation_error"); +const NoteMeta = require("../meta/note_meta"); +const AttachmentMeta = require("../meta/attachment_meta"); +const AttributeMeta = require("../meta/attribute_meta"); /** * @param {TaskContext} taskContext @@ -33,8 +36,14 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) zlib: { level: 9 } // Sets the compression level. }); + /** @type {Object.} */ const noteIdToMeta = {}; + /** + * @param {Object.} existingFileNames + * @param {string} fileName + * @returns {string} + */ function getUniqueFilename(existingFileNames, fileName) { const lcFileName = fileName.toLowerCase(); @@ -51,13 +60,20 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) return `${index}_${fileName}`; } - else { + else {[] existingFileNames[lcFileName] = 1; return fileName; } } + /** + * @param {string|null} type + * @param {string} mime + * @param {string} baseFileName + * @param {Object.} existingFileNames + * @return {string} + */ function getDataFileName(type, mime, baseFileName, existingFileNames) { let fileName = baseFileName; @@ -68,7 +84,7 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) fileName = fileName.substr(0, 30); } - // following two are handled specifically since we always want to have these extensions no matter the automatic detection + // the 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 (type === 'text' && format === 'markdown') { newExtension = 'md'; @@ -100,18 +116,24 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) return getUniqueFilename(existingFileNames, fileName); } + /** + * @param {BBranch} branch + * @param {NoteMeta} parentMeta + * @param {Object.} existingFileNames + * @returns {NoteMeta|null} + */ function getNoteMeta(branch, parentMeta, existingFileNames) { const note = branch.getNote(); if (note.hasOwnedLabel('excludeFromExport')) { - return; + return null; } const title = note.getTitleOrProtected(); const completeTitle = branch.prefix ? (`${branch.prefix} - ${title}`) : title; let baseFileName = sanitize(completeTitle); - if (baseFileName.length > 200) { // actual limit is 256 bytes(!) but let's be conservative + if (baseFileName.length > 200) { // the actual limit is 256 bytes(!) but let's be conservative baseFileName = baseFileName.substr(0, 200); } @@ -120,37 +142,37 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) if (note.noteId in noteIdToMeta) { const fileName = getUniqueFilename(existingFileNames, `${baseFileName}.clone.${format === 'html' ? 'html' : 'md'}`); - return { - isClone: true, - noteId: note.noteId, - notePath: notePath, - title: note.getTitleOrProtected(), - prefix: branch.prefix, - dataFileName: fileName, - type: 'text', // export will have text description, - format: format - }; + 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; + meta.type = 'text'; // export will have text description + meta.format = format; + return meta; } - const meta = { - isClone: false, - noteId: note.noteId, - notePath: notePath, - title: note.getTitleOrProtected(), - notePosition: branch.notePosition, - prefix: branch.prefix, - isExpanded: branch.isExpanded, - type: note.type, - mime: note.mime, - // we don't export utcDateCreated and utcDateModified of any entity since that would be a bit misleading - attributes: note.getOwnedAttributes().map(attribute => ({ - type: attribute.type, - name: attribute.name, - value: attribute.value, - isInheritable: attribute.isInheritable, - position: attribute.position - })) - }; + 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; + meta.isExpanded = branch.isExpanded; + meta.type = note.type; + meta.mime = note.mime; + meta.attributes = note.getOwnedAttributes().map(attribute => { + const attrMeta = new AttributeMeta(); + attrMeta.type = attribute.type; + attrMeta.name = attribute.name; + attrMeta.value = attribute.value; + attrMeta.isInheritable = attribute.isInheritable; + attrMeta.position = attribute.position; + return attrMeta; + }); taskContext.increaseProgressCount(); @@ -165,27 +187,27 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) const available = !note.isProtected || protectedSessionService.isProtectedSessionAvailable(); - // 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)) { meta.dataFileName = getDataFileName(note.type, note.mime, baseFileName, existingFileNames); } const attachments = note.getAttachments(); - - if (attachments.length > 0) { - meta.attachments = attachments - .map(attachment => ({ - title: attachment.title, - role: attachment.role, - mime: attachment.mime, - dataFileName: getDataFileName( + meta.attachments = attachments + .map(attachment => { + const attMeta = new AttachmentMeta(); + attMeta.attachmentId = attachment.attachmentId; + attMeta.title = attachment.title; + attMeta.role = attachment.role; + attMeta.mime = attachment.mime; + attMeta.dataFileName = getDataFileName( null, attachment.mime, baseFileName + "_" + attachment.title, existingFileNames - ) - })); - } + ); + return attMeta; + }); if (childBranches.length > 0) { meta.dirFileName = getUniqueFilename(existingFileNames, baseFileName); @@ -207,6 +229,11 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) return meta; } + /** + * @param {string} targetNoteId + * @param {NoteMeta} sourceMeta + * @return {string|null} + */ function getTargetUrl(targetNoteId, sourceMeta) { const targetMeta = noteIdToMeta[targetNoteId]; @@ -217,7 +244,7 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) const targetPath = targetMeta.notePath.slice(); const sourcePath = sourceMeta.notePath.slice(); - // > 1 for edge case that targetPath and sourcePath are exact same (link to itself) + // > 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]) { targetPath.shift(); sourcePath.shift(); @@ -233,12 +260,17 @@ 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 + // 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; } + /** + * @param {string} content + * @param {NoteMeta} noteMeta + * @return {string} + */ function findLinks(content, noteMeta) { content = content.replace(/src="[^"]*api\/images\/([a-zA-Z0-9_]+)\/[^"]*"/g, (match, targetNoteId) => { const url = getTargetUrl(targetNoteId, noteMeta); @@ -255,6 +287,12 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) return content; } + /** + * @param {string} title + * @param {string|Buffer} content + * @param {NoteMeta} noteMeta + * @return {string|Buffer} + */ function prepareContent(title, content, noteMeta) { if (['html', 'markdown'].includes(noteMeta.format)) { content = content.toString(); @@ -287,8 +325,7 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true) return content.length < 100000 ? html.prettyPrint(content, {indent_size: 2}) : content; - } - else if (noteMeta.format === 'markdown') { + } else if (noteMeta.format === 'markdown') { let markdownContent = mdService.toMarkdown(content); if (markdownContent.trim().length > 0 && !markdownContent.startsWith("# ")) { @@ -297,17 +334,17 @@ ${markdownContent}`; } return markdownContent; - } - else { + } else { return content; } } - // noteId => file path - const notePaths = {}; - + /** + * @param {NoteMeta} noteMeta + * @param {string} filePathPrefix + */ function saveNote(noteMeta, filePathPrefix) { - log.info(`Exporting note ${noteMeta.noteId}`); + log.info(`Exporting note '${noteMeta.noteId}'`); if (noteMeta.isClone) { const targetUrl = getTargetUrl(noteMeta.noteId, noteMeta); @@ -323,8 +360,6 @@ ${markdownContent}`; const note = becca.getNote(noteMeta.noteId); - notePaths[note.noteId] = filePathPrefix + (noteMeta.dataFileName || noteMeta.dirFileName); - if (noteMeta.dataFileName) { const content = prepareContent(noteMeta.title, note.getContent(), noteMeta); @@ -336,9 +371,8 @@ ${markdownContent}`; taskContext.increaseProgressCount(); - for (const attachmentMeta of noteMeta.attachments || []) { - // FIXME - const attachment = note.getAttachmentByName(attachmentMeta.name); + for (const attachmentMeta of noteMeta.attachments) { + const attachment = note.getAttachmentById(attachmentMeta.attachmentId); const content = attachment.getContent(); archive.append(content, { @@ -347,7 +381,7 @@ ${markdownContent}`; }); } - if (noteMeta.children && noteMeta.children.length > 0) { + if (noteMeta.children?.length > 0) { const directoryPath = filePathPrefix + noteMeta.dirFileName; // create directory @@ -359,6 +393,10 @@ ${markdownContent}`; } } + /** + * @param {NoteMeta} rootMeta + * @param {NoteMeta} navigationMeta + */ function saveNavigation(rootMeta, navigationMeta) { function saveNavigationInner(meta) { let html = '
  • '; @@ -401,6 +439,10 @@ ${markdownContent}`; archive.append(prettyHtml, { name: navigationMeta.dataFileName }); } + /** + * @param {NoteMeta} rootMeta + * @param {NoteMeta} indexMeta + */ function saveIndex(rootMeta, indexMeta) { let firstNonEmptyNote; let curMeta = rootMeta; @@ -433,6 +475,10 @@ ${markdownContent}`; archive.append(fullHtml, { name: indexMeta.dataFileName }); } + /** + * @param {NoteMeta} rootMeta + * @param {NoteMeta} cssMeta + */ function saveCss(rootMeta, cssMeta) { const cssContent = fs.readFileSync(`${RESOURCE_DIR}/libraries/ckeditor/ckeditor-content.css`); diff --git a/src/services/meta/attachment_meta.js b/src/services/meta/attachment_meta.js new file mode 100644 index 000000000..1e49f611d --- /dev/null +++ b/src/services/meta/attachment_meta.js @@ -0,0 +1,14 @@ +class AttachmentMeta { + /** @type {string} */ + attachmentId; + /** @type {string} */ + title; + /** @type {string} */ + role; + /** @type {string} */ + mime; + /** @type {string} */ + dataFileName; +} + +module.exports = AttachmentMeta; diff --git a/src/services/meta/attribute_meta.js b/src/services/meta/attribute_meta.js new file mode 100644 index 000000000..ca4cd144d --- /dev/null +++ b/src/services/meta/attribute_meta.js @@ -0,0 +1,14 @@ +class AttributeMeta { + /** @type {string} */ + type; + /** @type {string} */ + name; + /** @type {boolean} */ + value; + /** @type {boolean} */ + isInheritable; + /** @type {integer} */ + position; +} + +module.exports = AttributeMeta; diff --git a/src/services/meta/note_meta.js b/src/services/meta/note_meta.js new file mode 100644 index 000000000..917216704 --- /dev/null +++ b/src/services/meta/note_meta.js @@ -0,0 +1,34 @@ +class NoteMeta { + /** @type {string} */ + noteId; + /** @type {string} */ + notePath; + /** @type {boolean} */ + isClone; + /** @type {string} */ + title; + /** @type {integer} */ + notePosition; + /** @type {string} */ + prefix; + /** @type {boolean} */ + isExpanded; + /** @type {string} */ + type; + /** @type {string} */ + mime; + /** @type {string} - 'html' or 'markdown', applicable to text notes only */ + format; + /** @type {string} */ + dataFileName; + /** @type {string} */ + dirFileName; + /** @type {AttributeMeta[]} */ + attributes; + /** @type {AttachmentMeta[]} */ + attachments; + /** @type {NoteMeta[]|undefined} */ + children; +} + +module.exports = NoteMeta; diff --git a/src/services/options.js b/src/services/options.js index ccb622514..480b1e142 100644 --- a/src/services/options.js +++ b/src/services/options.js @@ -27,7 +27,7 @@ function getOption(name) { } /** - * @returns {number} + * @returns {integer} */ function getOptionInt(name) { const val = getOption(name); diff --git a/src/share/shaca/entities/sattribute.js b/src/share/shaca/entities/sattribute.js index dea4b950e..026a8eb01 100644 --- a/src/share/shaca/entities/sattribute.js +++ b/src/share/shaca/entities/sattribute.js @@ -14,7 +14,7 @@ class SAttribute extends AbstractShacaEntity { this.type = type; /** @param {string} */ this.name = name; - /** @param {int} */ + /** @param {integer} */ this.position = position; /** @param {string} */ this.value = value;