"use strict"; const Attribute = require('../../entities/attribute'); const utils = require('../../services/utils'); const log = require('../../services/log'); const repository = require('../../services/repository'); const noteService = require('../../services/notes'); const attributeService = require('../../services/attributes'); const Branch = require('../../entities/branch'); const tar = require('tar-stream'); const stream = require('stream'); const path = require('path'); const commonmark = require('commonmark'); const mimeTypes = require('mime-types'); const ImportContext = require('../import_context'); const protectedSessionService = require('../protected_session'); /** * @param {ImportContext} importContext * @param {Buffer} fileBuffer * @param {Note} importRootNote * @return {Promise<*>} */ async function importTar(importContext, fileBuffer, importRootNote) { // maps from original noteId (in tar file) to newly generated noteId const noteIdMap = {}; const attributes = []; // path => noteId const createdPaths = { '/': importRootNote.noteId, '\\': importRootNote.noteId }; const mdReader = new commonmark.Parser(); const mdWriter = new commonmark.HtmlRenderer(); let metaFile = null; let firstNote = null; const extract = tar.extract(); function getNewNoteId(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 (!noteIdMap[origNoteId]) { noteIdMap[origNoteId] = utils.newEntityId(); } return noteIdMap[origNoteId]; } function getMeta(filePath) { if (!metaFile) { return {}; } const pathSegments = filePath.split(/[\/\\]/g); let cursor = { isImportRoot: true, children: metaFile.files }; let parent; for (const segment of pathSegments) { if (!cursor || !cursor.children || cursor.children.length === 0) { return {}; } parent = cursor; cursor = cursor.children.find(file => file.dataFileName === segment || file.dirFileName === segment); } return { parentNoteMeta: parent, noteMeta: cursor }; } async function getParentNoteId(filePath, parentNoteMeta) { let parentNoteId; if (parentNoteMeta) { parentNoteId = parentNoteMeta.isImportRoot ? importRootNote.noteId : getNewNoteId(parentNoteMeta.noteId); } else { const parentPath = path.dirname(filePath); if (parentPath === '.') { parentNoteId = importRootNote.noteId; } else if (parentPath in createdPaths) { parentNoteId = createdPaths[parentPath]; } else { // tar allows creating out of order records - i.e. file in a directory can appear in the tar stream before actual directory // (out-of-order-directory-records.tar in test set) parentNoteId = await saveDirectory(parentPath); } } return parentNoteId; } function getNoteTitle(filePath, noteMeta) { if (noteMeta) { return noteMeta.title; } else { const basename = path.basename(filePath); return getTextFileWithoutExtension(basename); } } function getNoteId(noteMeta, filePath) { let noteId; if (noteMeta) { noteId = getNewNoteId(noteMeta.noteId); createdPaths[filePath] = noteId; } else { const filePathNoExt = getTextFileWithoutExtension(filePath); if (filePathNoExt in createdPaths) { noteId = createdPaths[filePathNoExt]; } else { noteId = utils.newEntityId(); } createdPaths[filePathNoExt] = noteId; } return noteId; } function detectFileTypeAndMime(filePath) { const mime = mimeTypes.lookup(filePath); let type = 'file'; if (mime) { if (mime === 'text/html' || ['text/markdown', 'text/x-markdown'].includes(mime)) { type = 'text'; } else if (mime.startsWith('image/')) { type = 'image'; } } return { type, mime }; } async function saveAttributes(note, noteMeta) { if (!noteMeta) { return; } for (const attr of noteMeta.attributes) { attr.noteId = note.noteId; if (!attributeService.isAttributeType(attr.type)) { log.error("Unrecognized attribute type " + attr.type); continue; } if (attr.type === 'relation') { attr.value = getNewNoteId(attr.value); } if (importContext.safeImport && attributeService.isAttributeDangerous(attr.type, attr.name)) { attr.name = 'disabled-' + attr.name; } attributes.push(attr); } } async function saveDirectory(filePath) { const { parentNoteMeta, noteMeta } = getMeta(filePath); const noteId = getNoteId(noteMeta, filePath); const noteTitle = getNoteTitle(filePath, noteMeta); const parentNoteId = await getParentNoteId(filePath, parentNoteMeta); let note = await repository.getNote(noteId); if (note) { return; } ({note} = await noteService.createNote(parentNoteId, noteTitle, '', { noteId, type: noteMeta ? noteMeta.type : 'text', mime: noteMeta ? noteMeta.mime : 'text/html', prefix: noteMeta ? noteMeta.prefix : '', isExpanded: noteMeta ? noteMeta.isExpanded : false, isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable(), })); await saveAttributes(note, noteMeta); if (!firstNote) { firstNote = note; } return noteId; } function getTextFileWithoutExtension(filePath) { const extension = path.extname(filePath).toLowerCase(); if (extension === '.md' || extension === '.html') { return filePath.substr(0, filePath.length - extension.length); } else { return filePath; } } function getNoteIdFromRelativeUrl(url, filePath) { while (url.startsWith("./")) { url = url.substr(2); } let absUrl = path.dirname(filePath); while (url.startsWith("../")) { absUrl = path.dirname(absUrl); url = url.substr(3); } if (absUrl === '.') { absUrl = ''; } absUrl += (absUrl.length > 0 ? '/' : '') + url; console.log("absUrl", absUrl); const targetNoteId = getNoteId(null, absUrl); return targetNoteId; } async function saveNote(filePath, content) { const {parentNoteMeta, noteMeta} = getMeta(filePath); const noteId = getNoteId(noteMeta, filePath); const parentNoteId = await getParentNoteId(filePath, parentNoteMeta); if (noteMeta && noteMeta.isClone) { await new Branch({ noteId, parentNoteId, isExpanded: noteMeta.isExpanded, prefix: noteMeta.prefix, notePosition: noteMeta.notePosition }).save(); return; } const {type, mime} = noteMeta ? noteMeta : detectFileTypeAndMime(filePath); if (type !== 'file' && type !== 'image') { content = content.toString("UTF-8"); } if ((noteMeta && noteMeta.format === 'markdown') || (!noteMeta && ['text/markdown', 'text/x-markdown'].includes(mime))) { const parsed = mdReader.parse(content); content = mdWriter.render(parsed); } const noteTitle = getNoteTitle(filePath, noteMeta); if (type === 'text') { function isUrlAbsolute(url) { return /^(?:[a-z]+:)?\/\//i.test(url); } content = content.replace(/src="([^"]*)"/g, (match, url) => { url = decodeURIComponent(url); if (isUrlAbsolute(url) || url.startsWith("/")) { return match; } const targetNoteId = getNoteIdFromRelativeUrl(url, filePath); return `src="api/images/${targetNoteId}/${path.basename(url)}"`; }); content = content.replace(/href="([^"]*)"/g, (match, url) => { url = decodeURIComponent(url); if (isUrlAbsolute(url)) { return match; } const targetNoteId = getNoteIdFromRelativeUrl(url, filePath); return `href="#root/${targetNoteId}"`; }); content = content.replace(/