2018-11-16 12:12:04 +01:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
const Attribute = require('../../entities/attribute');
|
|
|
|
const utils = require('../../services/utils');
|
2018-11-26 22:22:16 +01:00
|
|
|
const log = require('../../services/log');
|
2018-11-26 14:47:46 +01:00
|
|
|
const repository = require('../../services/repository');
|
2018-11-16 12:12:04 +01:00
|
|
|
const noteService = require('../../services/notes');
|
2019-02-11 23:45:58 +01:00
|
|
|
const attributeService = require('../../services/attributes');
|
2018-11-16 12:12:04 +01:00
|
|
|
const Branch = require('../../entities/branch');
|
|
|
|
const tar = require('tar-stream');
|
|
|
|
const stream = require('stream');
|
|
|
|
const path = require('path');
|
|
|
|
const commonmark = require('commonmark');
|
2018-11-26 14:47:46 +01:00
|
|
|
const mimeTypes = require('mime-types');
|
2019-02-20 23:07:57 +01:00
|
|
|
const ImportContext = require('../import_context');
|
2019-02-25 21:22:57 +01:00
|
|
|
const protectedSessionService = require('../protected_session');
|
2019-02-10 16:36:25 +01:00
|
|
|
|
2019-02-10 19:36:03 +01:00
|
|
|
/**
|
|
|
|
* @param {ImportContext} importContext
|
|
|
|
* @param {Buffer} fileBuffer
|
|
|
|
* @param {Note} importRootNote
|
|
|
|
* @return {Promise<*>}
|
|
|
|
*/
|
|
|
|
async function importTar(importContext, fileBuffer, importRootNote) {
|
2018-11-26 14:47:46 +01:00
|
|
|
// maps from original noteId (in tar file) to newly generated noteId
|
|
|
|
const noteIdMap = {};
|
2018-11-26 22:22:16 +01:00
|
|
|
const attributes = [];
|
2018-11-26 14:47:46 +01:00
|
|
|
// path => noteId
|
|
|
|
const createdPaths = { '/': importRootNote.noteId, '\\': importRootNote.noteId };
|
|
|
|
const mdReader = new commonmark.Parser();
|
|
|
|
const mdWriter = new commonmark.HtmlRenderer();
|
|
|
|
let metaFile = null;
|
|
|
|
let firstNote = null;
|
2018-11-16 12:12:04 +01:00
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
const extract = tar.extract();
|
|
|
|
|
|
|
|
function getNewNoteId(origNoteId) {
|
2018-11-16 12:12:04 +01:00
|
|
|
// in case the original noteId is empty. This probably shouldn't happen, but still good to have this precaution
|
|
|
|
if (!origNoteId.trim()) {
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
if (!noteIdMap[origNoteId]) {
|
|
|
|
noteIdMap[origNoteId] = utils.newEntityId();
|
2018-11-16 12:12:04 +01:00
|
|
|
}
|
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
return noteIdMap[origNoteId];
|
|
|
|
}
|
|
|
|
|
|
|
|
function getMeta(filePath) {
|
|
|
|
if (!metaFile) {
|
2018-11-26 23:39:43 +01:00
|
|
|
return {};
|
2018-11-26 14:47:46 +01:00
|
|
|
}
|
2018-11-16 12:12:04 +01:00
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
const pathSegments = filePath.split(/[\/\\]/g);
|
2018-11-16 12:12:04 +01:00
|
|
|
|
2018-11-26 20:30:43 +01:00
|
|
|
let cursor = {
|
|
|
|
isImportRoot: true,
|
|
|
|
children: metaFile.files
|
|
|
|
};
|
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
let parent;
|
2018-11-16 12:12:04 +01:00
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
for (const segment of pathSegments) {
|
|
|
|
if (!cursor || !cursor.children || cursor.children.length === 0) {
|
|
|
|
return {};
|
2018-11-16 12:12:04 +01:00
|
|
|
}
|
2018-11-26 14:47:46 +01:00
|
|
|
|
|
|
|
parent = cursor;
|
|
|
|
cursor = cursor.children.find(file => file.dataFileName === segment || file.dirFileName === segment);
|
2018-11-16 12:12:04 +01:00
|
|
|
}
|
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
return {
|
|
|
|
parentNoteMeta: parent,
|
|
|
|
noteMeta: cursor
|
|
|
|
};
|
2018-11-16 12:12:04 +01:00
|
|
|
}
|
|
|
|
|
2019-03-31 18:53:29 +02:00
|
|
|
async function getParentNoteId(filePath, parentNoteMeta) {
|
2018-11-26 14:47:46 +01:00
|
|
|
let parentNoteId;
|
2018-11-16 12:12:04 +01:00
|
|
|
|
2018-11-26 20:30:43 +01:00
|
|
|
if (parentNoteMeta) {
|
|
|
|
parentNoteId = parentNoteMeta.isImportRoot ? importRootNote.noteId : getNewNoteId(parentNoteMeta.noteId);
|
2018-11-16 12:12:04 +01:00
|
|
|
}
|
2018-11-26 14:47:46 +01:00
|
|
|
else {
|
|
|
|
const parentPath = path.dirname(filePath);
|
2018-11-16 12:12:04 +01:00
|
|
|
|
2018-11-26 23:39:43 +01:00
|
|
|
if (parentPath === '.') {
|
|
|
|
parentNoteId = importRootNote.noteId;
|
|
|
|
}
|
|
|
|
else if (parentPath in createdPaths) {
|
2018-11-26 14:47:46 +01:00
|
|
|
parentNoteId = createdPaths[parentPath];
|
|
|
|
}
|
|
|
|
else {
|
2019-03-31 18:53:29 +02:00
|
|
|
// 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);
|
2018-11-26 14:47:46 +01:00
|
|
|
}
|
|
|
|
}
|
2018-11-16 12:12:04 +01:00
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
return parentNoteId;
|
|
|
|
}
|
2018-11-16 12:12:04 +01:00
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
function getNoteTitle(filePath, noteMeta) {
|
|
|
|
if (noteMeta) {
|
|
|
|
return noteMeta.title;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
const basename = path.basename(filePath);
|
2018-11-16 12:12:04 +01:00
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
return getTextFileWithoutExtension(basename);
|
|
|
|
}
|
2018-11-16 12:12:04 +01:00
|
|
|
}
|
2018-11-26 14:47:46 +01:00
|
|
|
|
|
|
|
function getNoteId(noteMeta, filePath) {
|
|
|
|
if (noteMeta) {
|
|
|
|
return getNewNoteId(noteMeta.noteId);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
const filePathNoExt = getTextFileWithoutExtension(filePath);
|
|
|
|
|
|
|
|
if (filePathNoExt in createdPaths) {
|
|
|
|
return createdPaths[filePathNoExt];
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return utils.newEntityId();
|
|
|
|
}
|
|
|
|
}
|
2018-11-16 12:12:04 +01:00
|
|
|
}
|
2018-11-26 14:47:46 +01:00
|
|
|
|
|
|
|
function detectFileTypeAndMime(filePath) {
|
|
|
|
const mime = mimeTypes.lookup(filePath);
|
|
|
|
let type = 'file';
|
|
|
|
|
|
|
|
if (mime) {
|
2019-02-25 21:57:11 +01:00
|
|
|
if (mime === 'text/html' || ['text/markdown', 'text/x-markdown'].includes(mime)) {
|
2018-11-26 14:47:46 +01:00
|
|
|
type = 'text';
|
|
|
|
}
|
|
|
|
else if (mime.startsWith('image/')) {
|
|
|
|
type = 'image';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return { type, mime };
|
2018-11-16 12:12:04 +01:00
|
|
|
}
|
2018-11-26 14:47:46 +01:00
|
|
|
|
2019-08-19 20:12:00 +02:00
|
|
|
async function saveAttributes(note, noteMeta) {
|
2018-11-26 14:47:46 +01:00
|
|
|
if (!noteMeta) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const attr of noteMeta.attributes) {
|
2018-11-26 20:30:43 +01:00
|
|
|
attr.noteId = note.noteId;
|
|
|
|
|
2019-02-11 23:45:58 +01:00
|
|
|
if (!attributeService.isAttributeType(attr.type)) {
|
|
|
|
log.error("Unrecognized attribute type " + attr.type);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
if (attr.type === 'relation') {
|
|
|
|
attr.value = getNewNoteId(attr.value);
|
|
|
|
}
|
|
|
|
|
2019-02-11 23:45:58 +01:00
|
|
|
if (importContext.safeImport && attributeService.isAttributeDangerous(attr.type, attr.name)) {
|
|
|
|
attr.name = 'disabled-' + attr.name;
|
|
|
|
}
|
|
|
|
|
2018-11-26 22:22:16 +01:00
|
|
|
attributes.push(attr);
|
2018-11-26 14:47:46 +01:00
|
|
|
}
|
2018-11-16 12:12:04 +01:00
|
|
|
}
|
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
async function saveDirectory(filePath) {
|
|
|
|
const { parentNoteMeta, noteMeta } = getMeta(filePath);
|
2018-11-16 12:12:04 +01:00
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
const noteId = getNoteId(noteMeta, filePath);
|
|
|
|
const noteTitle = getNoteTitle(filePath, noteMeta);
|
2019-03-31 18:53:29 +02:00
|
|
|
const parentNoteId = await getParentNoteId(filePath, parentNoteMeta);
|
2018-11-16 12:12:04 +01:00
|
|
|
|
2018-11-26 20:30:43 +01:00
|
|
|
let note = await repository.getNote(noteId);
|
|
|
|
|
|
|
|
if (note) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
({note} = await noteService.createNote(parentNoteId, noteTitle, '', {
|
2018-11-26 14:47:46 +01:00
|
|
|
noteId,
|
|
|
|
type: noteMeta ? noteMeta.type : 'text',
|
|
|
|
mime: noteMeta ? noteMeta.mime : 'text/html',
|
|
|
|
prefix: noteMeta ? noteMeta.prefix : '',
|
2019-02-25 21:22:57 +01:00
|
|
|
isExpanded: noteMeta ? noteMeta.isExpanded : false,
|
|
|
|
isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
2018-11-26 20:30:43 +01:00
|
|
|
}));
|
2018-11-16 12:12:04 +01:00
|
|
|
|
2019-08-19 20:12:00 +02:00
|
|
|
await saveAttributes(note, noteMeta);
|
2018-11-26 14:47:46 +01:00
|
|
|
|
|
|
|
if (!firstNote) {
|
|
|
|
firstNote = note;
|
2018-11-16 12:12:04 +01:00
|
|
|
}
|
2018-11-26 14:47:46 +01:00
|
|
|
|
|
|
|
createdPaths[filePath] = noteId;
|
2019-03-31 18:53:29 +02:00
|
|
|
|
|
|
|
return noteId;
|
2018-11-26 14:47:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
function getTextFileWithoutExtension(filePath) {
|
|
|
|
const extension = path.extname(filePath).toLowerCase();
|
|
|
|
|
|
|
|
if (extension === '.md' || extension === '.html') {
|
|
|
|
return filePath.substr(0, filePath.length - extension.length);
|
2018-11-16 12:12:04 +01:00
|
|
|
}
|
|
|
|
else {
|
2018-11-26 14:47:46 +01:00
|
|
|
return filePath;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function saveNote(filePath, content) {
|
|
|
|
const {parentNoteMeta, noteMeta} = getMeta(filePath);
|
|
|
|
|
|
|
|
const noteId = getNoteId(noteMeta, filePath);
|
2019-03-31 18:53:29 +02:00
|
|
|
const parentNoteId = await getParentNoteId(filePath, parentNoteMeta);
|
2018-11-26 14:47:46 +01:00
|
|
|
|
|
|
|
if (noteMeta && noteMeta.isClone) {
|
|
|
|
await new Branch({
|
|
|
|
noteId,
|
|
|
|
parentNoteId,
|
|
|
|
isExpanded: noteMeta.isExpanded,
|
2018-11-26 23:39:43 +01:00
|
|
|
prefix: noteMeta.prefix,
|
|
|
|
notePosition: noteMeta.notePosition
|
2018-11-26 14:47:46 +01:00
|
|
|
}).save();
|
|
|
|
|
2018-11-16 12:12:04 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
const {type, mime} = noteMeta ? noteMeta : detectFileTypeAndMime(filePath);
|
|
|
|
|
2018-11-26 23:19:19 +01:00
|
|
|
if (type !== 'file' && type !== 'image') {
|
2018-11-26 14:47:46 +01:00
|
|
|
content = content.toString("UTF-8");
|
2018-11-26 23:19:19 +01:00
|
|
|
|
|
|
|
if (noteMeta) {
|
2019-08-30 21:22:52 +02:00
|
|
|
const internalLinks = (noteMeta.attributes || [])
|
|
|
|
.filter(attr => attr.type === 'relation' &&
|
|
|
|
['internal-link', 'relation-map-link', 'image-link'].includes(attr.name));
|
2019-08-19 20:12:00 +02:00
|
|
|
|
2018-11-26 23:19:19 +01:00
|
|
|
// 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)
|
2019-08-19 20:12:00 +02:00
|
|
|
for (const link of internalLinks) {
|
2018-11-26 23:19:19 +01:00
|
|
|
// no need to escape the regexp find string since it's a noteId which doesn't contain any special characters
|
2019-08-30 21:22:52 +02:00
|
|
|
content = content.replace(new RegExp(link.value, "g"), getNewNoteId(link.value));
|
2018-11-26 23:19:19 +01:00
|
|
|
}
|
|
|
|
}
|
2018-11-26 14:47:46 +01:00
|
|
|
}
|
|
|
|
|
2019-02-25 21:57:11 +01:00
|
|
|
if ((noteMeta && noteMeta.format === 'markdown') || (!noteMeta && ['text/markdown', 'text/x-markdown'].includes(mime))) {
|
2018-11-26 14:47:46 +01:00
|
|
|
const parsed = mdReader.parse(content);
|
|
|
|
content = mdWriter.render(parsed);
|
|
|
|
}
|
|
|
|
|
|
|
|
let note = await repository.getNote(noteId);
|
2018-11-16 12:12:04 +01:00
|
|
|
|
2018-11-26 20:30:43 +01:00
|
|
|
if (note) {
|
2019-03-26 22:24:04 +01:00
|
|
|
await note.setContent(content);
|
2018-11-26 20:30:43 +01:00
|
|
|
}
|
|
|
|
else {
|
2018-11-26 14:47:46 +01:00
|
|
|
const noteTitle = getNoteTitle(filePath, noteMeta);
|
2018-11-16 12:12:04 +01:00
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
({note} = await noteService.createNote(parentNoteId, noteTitle, content, {
|
|
|
|
noteId,
|
|
|
|
type,
|
|
|
|
mime,
|
|
|
|
prefix: noteMeta ? noteMeta.prefix : '',
|
2018-11-26 23:39:43 +01:00
|
|
|
isExpanded: noteMeta ? noteMeta.isExpanded : false,
|
2019-02-25 21:22:57 +01:00
|
|
|
notePosition: noteMeta ? noteMeta.notePosition : false,
|
|
|
|
isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
2018-11-26 14:47:46 +01:00
|
|
|
}));
|
2018-11-16 12:12:04 +01:00
|
|
|
|
2019-08-19 20:12:00 +02:00
|
|
|
await saveAttributes(note, noteMeta);
|
2018-11-26 14:47:46 +01:00
|
|
|
|
2018-11-26 23:39:43 +01:00
|
|
|
if (!noteMeta && (type === 'file' || type === 'image')) {
|
|
|
|
attributes.push({
|
|
|
|
noteId,
|
|
|
|
type: 'label',
|
|
|
|
name: 'originalFileName',
|
|
|
|
value: path.basename(filePath)
|
|
|
|
});
|
|
|
|
|
|
|
|
attributes.push({
|
|
|
|
noteId,
|
|
|
|
type: 'label',
|
|
|
|
name: 'fileSize',
|
|
|
|
value: content.byteLength
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
if (!firstNote) {
|
|
|
|
firstNote = note;
|
2018-11-16 12:12:04 +01:00
|
|
|
}
|
2018-11-26 14:47:46 +01:00
|
|
|
|
|
|
|
if (type === 'text') {
|
|
|
|
filePath = getTextFileWithoutExtension(filePath);
|
2018-11-16 12:12:04 +01:00
|
|
|
}
|
2018-11-26 14:47:46 +01:00
|
|
|
|
|
|
|
createdPaths[filePath] = noteId;
|
2018-11-16 12:12:04 +01:00
|
|
|
}
|
2018-11-26 20:30:43 +01:00
|
|
|
}
|
|
|
|
|
2019-02-10 16:36:25 +01:00
|
|
|
/** @return {string} path without leading or trailing slash and backslashes converted to forward ones*/
|
2018-11-26 20:30:43 +01:00
|
|
|
function normalizeFilePath(filePath) {
|
|
|
|
filePath = filePath.replace(/\\/g, "/");
|
|
|
|
|
|
|
|
if (filePath.startsWith("/")) {
|
|
|
|
filePath = filePath.substr(1);
|
2018-11-26 14:47:46 +01:00
|
|
|
}
|
2018-11-26 20:30:43 +01:00
|
|
|
|
|
|
|
if (filePath.endsWith("/")) {
|
|
|
|
filePath = filePath.substr(0, filePath.length - 1);
|
|
|
|
}
|
2018-11-26 22:22:16 +01:00
|
|
|
|
2018-11-26 20:30:43 +01:00
|
|
|
return filePath;
|
2018-11-26 14:47:46 +01:00
|
|
|
}
|
2018-11-16 12:12:04 +01:00
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
extract.on('entry', function(header, stream, next) {
|
2018-11-16 12:12:04 +01:00
|
|
|
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
|
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
stream.on('end', async function() {
|
2019-02-10 16:36:25 +01:00
|
|
|
const filePath = normalizeFilePath(header.name);
|
2018-11-26 20:30:43 +01:00
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
const content = Buffer.concat(chunks);
|
2018-11-16 12:12:04 +01:00
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
if (filePath === '!!!meta.json') {
|
|
|
|
metaFile = JSON.parse(content.toString("UTF-8"));
|
|
|
|
}
|
|
|
|
else if (header.type === 'directory') {
|
|
|
|
await saveDirectory(filePath);
|
|
|
|
}
|
2018-11-26 22:22:16 +01:00
|
|
|
else if (header.type === 'file') {
|
2018-11-26 14:47:46 +01:00
|
|
|
await saveNote(filePath, content);
|
2018-11-16 12:12:04 +01:00
|
|
|
}
|
2018-11-26 22:22:16 +01:00
|
|
|
else {
|
|
|
|
log.info("Ignoring tar import entry with type " + header.type);
|
|
|
|
}
|
2018-11-16 12:12:04 +01:00
|
|
|
|
2019-02-10 22:30:55 +01:00
|
|
|
importContext.increaseProgressCount();
|
2019-02-10 16:36:25 +01:00
|
|
|
|
2018-11-16 12:12:04 +01:00
|
|
|
next(); // ready for next entry
|
|
|
|
});
|
|
|
|
|
|
|
|
stream.resume(); // just auto drain the stream
|
|
|
|
});
|
|
|
|
|
|
|
|
return new Promise(resolve => {
|
2018-11-26 22:22:16 +01:00
|
|
|
extract.on('finish', async function() {
|
|
|
|
const createdNoteIds = {};
|
|
|
|
|
|
|
|
for (const path in createdPaths) {
|
|
|
|
createdNoteIds[createdPaths[path]] = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// we're saving attributes and links only now so that all relation and link target notes
|
|
|
|
// are already in the database (we don't want to have "broken" relations, not even transitionally)
|
|
|
|
for (const attr of attributes) {
|
2018-11-26 23:19:19 +01:00
|
|
|
if (attr.type !== 'relation' || attr.value in createdNoteIds) {
|
2018-11-26 22:22:16 +01:00
|
|
|
await new Attribute(attr).save();
|
|
|
|
}
|
2018-11-26 23:19:19 +01:00
|
|
|
else {
|
|
|
|
log.info("Relation not imported since target note doesn't exist: " + JSON.stringify(attr));
|
|
|
|
}
|
2018-11-26 22:22:16 +01:00
|
|
|
}
|
|
|
|
|
2018-11-26 14:47:46 +01:00
|
|
|
resolve(firstNote);
|
2018-11-16 12:12:04 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
const bufferStream = new stream.PassThrough();
|
2018-11-16 14:36:50 +01:00
|
|
|
bufferStream.end(fileBuffer);
|
2018-11-16 12:12:04 +01:00
|
|
|
|
|
|
|
bufferStream.pipe(extract);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
importTar
|
|
|
|
};
|