mirror of
https://github.com/zadam/trilium.git
synced 2025-06-06 18:08:33 +02:00
server-ts: Port services/import/zip
This commit is contained in:
parent
764d251b0a
commit
53d4873c1f
@ -62,7 +62,7 @@ export interface BlobRow {
|
|||||||
utcDateModified: string;
|
utcDateModified: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AttributeType = "label" | "relation";
|
export type AttributeType = "label" | "relation" | "label-definition" | "relation-definition";
|
||||||
|
|
||||||
export interface AttributeRow {
|
export interface AttributeRow {
|
||||||
attributeId?: string;
|
attributeId?: string;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import mimeTypes = require('mime-types');
|
import mimeTypes = require('mime-types');
|
||||||
import path = require('path');
|
import path = require('path');
|
||||||
|
import { TaskData } from '../task_context_interface';
|
||||||
|
|
||||||
const CODE_MIME_TYPES: Record<string, boolean | string> = {
|
const CODE_MIME_TYPES: Record<string, boolean | string> = {
|
||||||
'text/plain': true,
|
'text/plain': true,
|
||||||
@ -79,12 +80,7 @@ function getMime(fileName: string) {
|
|||||||
return mimeTypes.lookup(fileName);
|
return mimeTypes.lookup(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetTypeOpts {
|
function getType(options: TaskData, mime: string) {
|
||||||
textImportedAsText?: boolean;
|
|
||||||
codeImportedAsCode?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getType(options: GetTypeOpts, mime: string) {
|
|
||||||
mime = mime ? mime.toLowerCase() : '';
|
mime = mime ? mime.toLowerCase() : '';
|
||||||
|
|
||||||
if (options.textImportedAsText && (mime === 'text/html' || ['text/markdown', 'text/x-markdown'].includes(mime))) {
|
if (options.textImportedAsText && (mime === 'text/html' || ['text/markdown', 'text/x-markdown'].includes(mime))) {
|
||||||
|
@ -1,43 +1,45 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const BAttribute = require('../../becca/entities/battribute');
|
import BAttribute = require('../../becca/entities/battribute');
|
||||||
const utils = require('../../services/utils');
|
import utils = require('../../services/utils');
|
||||||
const log = require('../../services/log');
|
import log = require('../../services/log');
|
||||||
const noteService = require('../../services/notes');
|
import noteService = require('../../services/notes');
|
||||||
const attributeService = require('../../services/attributes');
|
import attributeService = require('../../services/attributes');
|
||||||
const BBranch = require('../../becca/entities/bbranch');
|
import BBranch = require('../../becca/entities/bbranch');
|
||||||
const path = require('path');
|
import path = require('path');
|
||||||
const protectedSessionService = require('../protected_session');
|
import protectedSessionService = require('../protected_session');
|
||||||
const mimeService = require('./mime');
|
import mimeService = require('./mime');
|
||||||
const treeService = require('../tree');
|
import treeService = require('../tree');
|
||||||
const yauzl = require("yauzl");
|
import yauzl = require("yauzl");
|
||||||
const htmlSanitizer = require('../html_sanitizer');
|
import htmlSanitizer = require('../html_sanitizer');
|
||||||
const becca = require('../../becca/becca');
|
import becca = require('../../becca/becca');
|
||||||
const BAttachment = require('../../becca/entities/battachment');
|
import BAttachment = require('../../becca/entities/battachment');
|
||||||
const markdownService = require('./markdown');
|
import markdownService = require('./markdown');
|
||||||
|
import TaskContext = require('../task_context');
|
||||||
|
import BNote = require('../../becca/entities/bnote');
|
||||||
|
import NoteMeta = require('../meta/note_meta');
|
||||||
|
import AttributeMeta = require('../meta/attribute_meta');
|
||||||
|
import { Stream } from 'stream';
|
||||||
|
import { NoteType } from '../../becca/entities/rows';
|
||||||
|
|
||||||
/**
|
interface MetaFile {
|
||||||
* @param {TaskContext} taskContext
|
files: NoteMeta[]
|
||||||
* @param {Buffer} fileBuffer
|
}
|
||||||
* @param {BNote} importRootNote
|
|
||||||
* @returns {Promise<BNote>}
|
async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRootNote: BNote): Promise<BNote> {
|
||||||
*/
|
/** maps from original noteId (in ZIP file) to newly generated noteId */
|
||||||
async function importZip(taskContext, fileBuffer, importRootNote) {
|
const noteIdMap: Record<string, string> = {};
|
||||||
/** @type {Object.<string, string>} maps from original noteId (in ZIP file) to newly generated noteId */
|
/** type maps from original attachmentId (in ZIP file) to newly generated attachmentId */
|
||||||
const noteIdMap = {};
|
const attachmentIdMap: Record<string, string> = {};
|
||||||
/** @type {Object.<string, string>} maps from original attachmentId (in ZIP file) to newly generated attachmentId */
|
const attributes: AttributeMeta[] = [];
|
||||||
const attachmentIdMap = {};
|
|
||||||
const attributes = [];
|
|
||||||
// path => noteId, used only when meta file is not available
|
// path => noteId, used only when meta file is not available
|
||||||
/** @type {Object.<string, string>} path => noteId | attachmentId */
|
/** path => noteId | attachmentId */
|
||||||
const createdPaths = { '/': importRootNote.noteId, '\\': importRootNote.noteId };
|
const createdPaths: Record<string, string> = { '/': importRootNote.noteId, '\\': importRootNote.noteId };
|
||||||
let metaFile = null;
|
let metaFile!: MetaFile;
|
||||||
/** @type {BNote} */
|
let firstNote!: BNote;
|
||||||
let firstNote = null;
|
const createdNoteIds = new Set<string>();
|
||||||
/** @type {Set.<string>} */
|
|
||||||
const createdNoteIds = new Set();
|
|
||||||
|
|
||||||
function getNewNoteId(origNoteId) {
|
function getNewNoteId(origNoteId: string) {
|
||||||
if (!origNoteId.trim()) {
|
if (!origNoteId.trim()) {
|
||||||
// this probably shouldn't happen, but still good to have this precaution
|
// this probably shouldn't happen, but still good to have this precaution
|
||||||
return "empty_note_id";
|
return "empty_note_id";
|
||||||
@ -55,7 +57,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
return noteIdMap[origNoteId];
|
return noteIdMap[origNoteId];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNewAttachmentId(origAttachmentId) {
|
function getNewAttachmentId(origAttachmentId: string) {
|
||||||
if (!origAttachmentId.trim()) {
|
if (!origAttachmentId.trim()) {
|
||||||
// this probably shouldn't happen, but still good to have this precaution
|
// this probably shouldn't happen, but still good to have this precaution
|
||||||
return "empty_attachment_id";
|
return "empty_attachment_id";
|
||||||
@ -68,12 +70,8 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
return attachmentIdMap[origAttachmentId];
|
return attachmentIdMap[origAttachmentId];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function getAttachmentMeta(parentNoteMeta: NoteMeta, dataFileName: string) {
|
||||||
* @param {NoteMeta} parentNoteMeta
|
for (const noteMeta of parentNoteMeta.children || []) {
|
||||||
* @param {string} dataFileName
|
|
||||||
*/
|
|
||||||
function getAttachmentMeta(parentNoteMeta, dataFileName) {
|
|
||||||
for (const noteMeta of parentNoteMeta.children) {
|
|
||||||
for (const attachmentMeta of noteMeta.attachments || []) {
|
for (const attachmentMeta of noteMeta.attachments || []) {
|
||||||
if (attachmentMeta.dataFileName === dataFileName) {
|
if (attachmentMeta.dataFileName === dataFileName) {
|
||||||
return {
|
return {
|
||||||
@ -88,22 +86,20 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @returns {{noteMeta: NoteMeta|undefined, parentNoteMeta: NoteMeta|undefined, attachmentMeta: AttachmentMeta|undefined}} */
|
function getMeta(filePath: string) {
|
||||||
function getMeta(filePath) {
|
|
||||||
if (!metaFile) {
|
if (!metaFile) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathSegments = filePath.split(/[\/\\]/g);
|
const pathSegments = filePath.split(/[\/\\]/g);
|
||||||
|
|
||||||
/** @type {NoteMeta} */
|
let cursor: NoteMeta | undefined = {
|
||||||
let cursor = {
|
|
||||||
isImportRoot: true,
|
isImportRoot: true,
|
||||||
children: metaFile.files
|
children: metaFile.files,
|
||||||
|
dataFileName: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {NoteMeta} */
|
let parent!: NoteMeta;
|
||||||
let parent;
|
|
||||||
|
|
||||||
for (const segment of pathSegments) {
|
for (const segment of pathSegments) {
|
||||||
if (!cursor?.children?.length) {
|
if (!cursor?.children?.length) {
|
||||||
@ -111,7 +107,9 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parent = cursor;
|
parent = cursor;
|
||||||
cursor = parent.children.find(file => file.dataFileName === segment || file.dirFileName === segment);
|
if (parent.children) {
|
||||||
|
cursor = parent.children.find(file => file.dataFileName === segment || file.dirFileName === segment);
|
||||||
|
}
|
||||||
|
|
||||||
if (!cursor) {
|
if (!cursor) {
|
||||||
return getAttachmentMeta(parent, segment);
|
return getAttachmentMeta(parent, segment);
|
||||||
@ -120,19 +118,15 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
parentNoteMeta: parent,
|
parentNoteMeta: parent,
|
||||||
noteMeta: cursor
|
noteMeta: cursor,
|
||||||
|
attachmentMeta: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function getParentNoteId(filePath: string, parentNoteMeta?: NoteMeta) {
|
||||||
* @param {string} filePath
|
|
||||||
* @param {NoteMeta} parentNoteMeta
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
function getParentNoteId(filePath, parentNoteMeta) {
|
|
||||||
let parentNoteId;
|
let parentNoteId;
|
||||||
|
|
||||||
if (parentNoteMeta) {
|
if (parentNoteMeta?.noteId) {
|
||||||
parentNoteId = parentNoteMeta.isImportRoot ? importRootNote.noteId : getNewNoteId(parentNoteMeta.noteId);
|
parentNoteId = parentNoteMeta.isImportRoot ? importRootNote.noteId : getNewNoteId(parentNoteMeta.noteId);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -151,13 +145,8 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
return parentNoteId;
|
return parentNoteId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function getNoteId(noteMeta: NoteMeta | undefined, filePath: string): string {
|
||||||
* @param {NoteMeta} noteMeta
|
if (noteMeta?.noteId) {
|
||||||
* @param {string} filePath
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
function getNoteId(noteMeta, filePath) {
|
|
||||||
if (noteMeta) {
|
|
||||||
return getNewNoteId(noteMeta.noteId);
|
return getNewNoteId(noteMeta.noteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,23 +165,19 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
return noteId;
|
return noteId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectFileTypeAndMime(taskContext, filePath) {
|
function detectFileTypeAndMime(taskContext: TaskContext, filePath: string) {
|
||||||
const mime = mimeService.getMime(filePath) || "application/octet-stream";
|
const mime = mimeService.getMime(filePath) || "application/octet-stream";
|
||||||
const type = mimeService.getType(taskContext.data, mime);
|
const type = mimeService.getType(taskContext.data || {}, mime);
|
||||||
|
|
||||||
return { mime, type };
|
return { mime, type };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function saveAttributes(note: BNote, noteMeta: NoteMeta | undefined) {
|
||||||
* @param {BNote} note
|
|
||||||
* @param {NoteMeta} noteMeta
|
|
||||||
*/
|
|
||||||
function saveAttributes(note, noteMeta) {
|
|
||||||
if (!noteMeta) {
|
if (!noteMeta) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const attr of noteMeta.attributes) {
|
for (const attr of noteMeta.attributes || []) {
|
||||||
attr.noteId = note.noteId;
|
attr.noteId = note.noteId;
|
||||||
|
|
||||||
if (attr.type === 'label-definition') {
|
if (attr.type === 'label-definition') {
|
||||||
@ -218,11 +203,11 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
attr.value = getNewNoteId(attr.value);
|
attr.value = getNewNoteId(attr.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (taskContext.data.safeImport && attributeService.isAttributeDangerous(attr.type, attr.name)) {
|
if (taskContext.data?.safeImport && attributeService.isAttributeDangerous(attr.type, attr.name)) {
|
||||||
attr.name = `disabled:${attr.name}`;
|
attr.name = `disabled:${attr.name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (taskContext.data.safeImport) {
|
if (taskContext.data?.safeImport) {
|
||||||
attr.name = htmlSanitizer.sanitize(attr.name);
|
attr.name = htmlSanitizer.sanitize(attr.name);
|
||||||
attr.value = htmlSanitizer.sanitize(attr.value);
|
attr.value = htmlSanitizer.sanitize(attr.value);
|
||||||
}
|
}
|
||||||
@ -231,7 +216,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveDirectory(filePath) {
|
function saveDirectory(filePath: string) {
|
||||||
const { parentNoteMeta, noteMeta } = getMeta(filePath);
|
const { parentNoteMeta, noteMeta } = getMeta(filePath);
|
||||||
|
|
||||||
const noteId = getNoteId(noteMeta, filePath);
|
const noteId = getNoteId(noteMeta, filePath);
|
||||||
@ -240,12 +225,16 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const noteTitle = utils.getNoteTitle(filePath, taskContext.data.replaceUnderscoresWithSpaces, noteMeta);
|
const noteTitle = utils.getNoteTitle(filePath, !!taskContext.data?.replaceUnderscoresWithSpaces, noteMeta);
|
||||||
const parentNoteId = getParentNoteId(filePath, parentNoteMeta);
|
const parentNoteId = getParentNoteId(filePath, parentNoteMeta);
|
||||||
|
|
||||||
|
if (!parentNoteId) {
|
||||||
|
throw new Error("Missing parent note ID.");
|
||||||
|
}
|
||||||
|
|
||||||
const {note} = noteService.createNewNote({
|
const {note} = noteService.createNewNote({
|
||||||
parentNoteId: parentNoteId,
|
parentNoteId: parentNoteId,
|
||||||
title: noteTitle,
|
title: noteTitle || "",
|
||||||
content: '',
|
content: '',
|
||||||
noteId: noteId,
|
noteId: noteId,
|
||||||
type: resolveNoteType(noteMeta?.type),
|
type: resolveNoteType(noteMeta?.type),
|
||||||
@ -265,8 +254,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
return noteId;
|
return noteId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @returns {{attachmentId: string}|{noteId: string}} */
|
function getEntityIdFromRelativeUrl(url: string, filePath: string) {
|
||||||
function getEntityIdFromRelativeUrl(url, filePath) {
|
|
||||||
while (url.startsWith("./")) {
|
while (url.startsWith("./")) {
|
||||||
url = url.substr(2);
|
url = url.substr(2);
|
||||||
}
|
}
|
||||||
@ -287,7 +275,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
|
|
||||||
const { noteMeta, attachmentMeta } = getMeta(absUrl);
|
const { noteMeta, attachmentMeta } = getMeta(absUrl);
|
||||||
|
|
||||||
if (attachmentMeta) {
|
if (attachmentMeta && attachmentMeta.attachmentId && noteMeta.noteId) {
|
||||||
return {
|
return {
|
||||||
attachmentId: getNewAttachmentId(attachmentMeta.attachmentId),
|
attachmentId: getNewAttachmentId(attachmentMeta.attachmentId),
|
||||||
noteId: getNewNoteId(noteMeta.noteId)
|
noteId: getNewNoteId(noteMeta.noteId)
|
||||||
@ -299,15 +287,8 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function processTextNoteContent(content: string, noteTitle: string, filePath: string, noteMeta?: NoteMeta) {
|
||||||
* @param {string} content
|
function isUrlAbsolute(url: string) {
|
||||||
* @param {string} noteTitle
|
|
||||||
* @param {string} filePath
|
|
||||||
* @param {NoteMeta} noteMeta
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
function processTextNoteContent(content, noteTitle, filePath, noteMeta) {
|
|
||||||
function isUrlAbsolute(url) {
|
|
||||||
return /^(?:[a-z]+:)?\/\//i.test(url);
|
return /^(?:[a-z]+:)?\/\//i.test(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,7 +302,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (taskContext.data.safeImport) {
|
if (taskContext.data?.safeImport) {
|
||||||
content = htmlSanitizer.sanitize(content);
|
content = htmlSanitizer.sanitize(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,7 +317,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
url = decodeURIComponent(url).trim();
|
url = decodeURIComponent(url).trim();
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
log.error(`Cannot parse image URL '${url}', keeping original. Error: ${e.message}.`);
|
log.error(`Cannot parse image URL '${url}', keeping original. Error: ${e.message}.`);
|
||||||
return `src="${url}"`;
|
return `src="${url}"`;
|
||||||
}
|
}
|
||||||
@ -359,7 +340,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
content = content.replace(/href="([^"]*)"/g, (match, url) => {
|
content = content.replace(/href="([^"]*)"/g, (match, url) => {
|
||||||
try {
|
try {
|
||||||
url = decodeURIComponent(url).trim();
|
url = decodeURIComponent(url).trim();
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
log.error(`Cannot parse link URL '${url}', keeping original. Error: ${e.message}.`);
|
log.error(`Cannot parse link URL '${url}', keeping original. Error: ${e.message}.`);
|
||||||
return `href="${url}"`;
|
return `href="${url}"`;
|
||||||
}
|
}
|
||||||
@ -395,7 +376,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeTriliumTags(content) {
|
function removeTriliumTags(content: string) {
|
||||||
const tagsToRemove = [
|
const tagsToRemove = [
|
||||||
'<h1 data-trilium-h1>([^<]*)<\/h1>',
|
'<h1 data-trilium-h1>([^<]*)<\/h1>',
|
||||||
'<title data-trilium-title>([^<]*)<\/title>'
|
'<title data-trilium-title>([^<]*)<\/title>'
|
||||||
@ -407,26 +388,18 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function processNoteContent(noteMeta: NoteMeta | undefined, type: string, mime: string, content: string | Buffer, noteTitle: string, filePath: string) {
|
||||||
* @param {NoteMeta} noteMeta
|
if ((noteMeta?.format === 'markdown'
|
||||||
* @param {string} type
|
|| (!noteMeta && taskContext.data?.textImportedAsText && ['text/markdown', 'text/x-markdown'].includes(mime)))
|
||||||
* @param {string} mime
|
&& typeof content === "string") {
|
||||||
* @param {string|Buffer} content
|
|
||||||
* @param {string} noteTitle
|
|
||||||
* @param {string} filePath
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
function processNoteContent(noteMeta, type, mime, content, noteTitle, filePath) {
|
|
||||||
if (noteMeta?.format === 'markdown'
|
|
||||||
|| (!noteMeta && taskContext.data.textImportedAsText && ['text/markdown', 'text/x-markdown'].includes(mime))) {
|
|
||||||
content = markdownService.renderToHtml(content, noteTitle);
|
content = markdownService.renderToHtml(content, noteTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'text') {
|
if (type === 'text' && typeof content === "string") {
|
||||||
content = processTextNoteContent(content, noteTitle, filePath, noteMeta);
|
content = processTextNoteContent(content, noteTitle, filePath, noteMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'relationMap' && noteMeta) {
|
if (type === 'relationMap' && noteMeta && typeof content === "string") {
|
||||||
const relationMapLinks = (noteMeta.attributes || [])
|
const relationMapLinks = (noteMeta.attributes || [])
|
||||||
.filter(attr => attr.type === 'relation' && attr.name === 'relationMapLink');
|
.filter(attr => attr.type === 'relation' && attr.name === 'relationMapLink');
|
||||||
|
|
||||||
@ -440,11 +413,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function saveNote(filePath: string, content: string | Buffer) {
|
||||||
* @param {string} filePath
|
|
||||||
* @param {Buffer} content
|
|
||||||
*/
|
|
||||||
function saveNote(filePath, content) {
|
|
||||||
const { parentNoteMeta, noteMeta, attachmentMeta } = getMeta(filePath);
|
const { parentNoteMeta, noteMeta, attachmentMeta } = getMeta(filePath);
|
||||||
|
|
||||||
if (noteMeta?.noImport) {
|
if (noteMeta?.noImport) {
|
||||||
@ -453,7 +422,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
|
|
||||||
const noteId = getNoteId(noteMeta, filePath);
|
const noteId = getNoteId(noteMeta, filePath);
|
||||||
|
|
||||||
if (attachmentMeta) {
|
if (attachmentMeta && attachmentMeta.attachmentId) {
|
||||||
const attachment = new BAttachment({
|
const attachment = new BAttachment({
|
||||||
attachmentId: getNewAttachmentId(attachmentMeta.attachmentId),
|
attachmentId: getNewAttachmentId(attachmentMeta.attachmentId),
|
||||||
ownerId: noteId,
|
ownerId: noteId,
|
||||||
@ -487,16 +456,20 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { type, mime } = noteMeta ? noteMeta : detectFileTypeAndMime(taskContext, filePath);
|
let { mime } = noteMeta ? noteMeta : detectFileTypeAndMime(taskContext, filePath);
|
||||||
type = resolveNoteType(type);
|
if (!mime) {
|
||||||
|
throw new Error("Unable to resolve mime type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let type = resolveNoteType(noteMeta?.type);
|
||||||
|
|
||||||
if (type !== 'file' && type !== 'image') {
|
if (type !== 'file' && type !== 'image') {
|
||||||
content = content.toString("utf-8");
|
content = content.toString("utf-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
const noteTitle = utils.getNoteTitle(filePath, taskContext.data.replaceUnderscoresWithSpaces, noteMeta);
|
const noteTitle = utils.getNoteTitle(filePath, taskContext.data?.replaceUnderscoresWithSpaces || false, noteMeta);
|
||||||
|
|
||||||
content = processNoteContent(noteMeta, type, mime, content, noteTitle, filePath);
|
content = processNoteContent(noteMeta, type, mime, content, noteTitle || "", filePath);
|
||||||
|
|
||||||
let note = becca.getNote(noteId);
|
let note = becca.getNote(noteId);
|
||||||
|
|
||||||
@ -508,7 +481,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
if (note.type === undefined) {
|
if (note.type === undefined) {
|
||||||
note.type = type;
|
note.type = type;
|
||||||
note.mime = mime;
|
note.mime = mime;
|
||||||
note.title = noteTitle;
|
note.title = noteTitle || "";
|
||||||
note.isProtected = isProtected;
|
note.isProtected = isProtected;
|
||||||
note.save();
|
note.save();
|
||||||
}
|
}
|
||||||
@ -519,16 +492,20 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
new BBranch({
|
new BBranch({
|
||||||
noteId,
|
noteId,
|
||||||
parentNoteId,
|
parentNoteId,
|
||||||
isExpanded: noteMeta.isExpanded,
|
isExpanded: noteMeta?.isExpanded,
|
||||||
prefix: noteMeta.prefix,
|
prefix: noteMeta?.prefix,
|
||||||
notePosition: noteMeta.notePosition
|
notePosition: noteMeta?.notePosition
|
||||||
}).save();
|
}).save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
if (typeof content !== "string") {
|
||||||
|
throw new Error("Incorrect content type.");
|
||||||
|
}
|
||||||
|
|
||||||
({note} = noteService.createNewNote({
|
({note} = noteService.createNewNote({
|
||||||
parentNoteId: parentNoteId,
|
parentNoteId: parentNoteId,
|
||||||
title: noteTitle,
|
title: noteTitle || "",
|
||||||
content: content,
|
content: content,
|
||||||
noteId,
|
noteId,
|
||||||
type,
|
type,
|
||||||
@ -560,7 +537,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
|
|
||||||
// we're running two passes to make sure that the meta file is loaded before the rest of the files is processed.
|
// we're running two passes to make sure that the meta file is loaded before the rest of the files is processed.
|
||||||
|
|
||||||
await readZipFile(fileBuffer, async (zipfile, entry) => {
|
await readZipFile(fileBuffer, async (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => {
|
||||||
const filePath = normalizeFilePath(entry.fileName);
|
const filePath = normalizeFilePath(entry.fileName);
|
||||||
|
|
||||||
if (filePath === '!!!meta.json') {
|
if (filePath === '!!!meta.json') {
|
||||||
@ -572,7 +549,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
zipfile.readEntry();
|
zipfile.readEntry();
|
||||||
});
|
});
|
||||||
|
|
||||||
await readZipFile(fileBuffer, async (zipfile, entry) => {
|
await readZipFile(fileBuffer, async (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => {
|
||||||
const filePath = normalizeFilePath(entry.fileName);
|
const filePath = normalizeFilePath(entry.fileName);
|
||||||
|
|
||||||
if (/\/$/.test(entry.fileName)) {
|
if (/\/$/.test(entry.fileName)) {
|
||||||
@ -590,6 +567,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
|
|
||||||
for (const noteId of createdNoteIds) {
|
for (const noteId of createdNoteIds) {
|
||||||
const note = becca.getNote(noteId);
|
const note = becca.getNote(noteId);
|
||||||
|
if (!note) continue;
|
||||||
await noteService.asyncPostProcessContent(note, note.getContent());
|
await noteService.asyncPostProcessContent(note, note.getContent());
|
||||||
|
|
||||||
if (!metaFile) {
|
if (!metaFile) {
|
||||||
@ -615,8 +593,8 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
return firstNote;
|
return firstNote;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @returns {string} path without leading or trailing slash and backslashes converted to forward ones */
|
/** @returns path without leading or trailing slash and backslashes converted to forward ones */
|
||||||
function normalizeFilePath(filePath) {
|
function normalizeFilePath(filePath: string): string {
|
||||||
filePath = filePath.replace(/\\/g, "/");
|
filePath = filePath.replace(/\\/g, "/");
|
||||||
|
|
||||||
if (filePath.startsWith("/")) {
|
if (filePath.startsWith("/")) {
|
||||||
@ -630,29 +608,30 @@ function normalizeFilePath(filePath) {
|
|||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @returns {Promise<Buffer>} */
|
function streamToBuffer(stream: Stream): Promise<Buffer> {
|
||||||
function streamToBuffer(stream) {
|
const chunks: Uint8Array[] = [];
|
||||||
const chunks = [];
|
|
||||||
stream.on('data', chunk => chunks.push(chunk));
|
stream.on('data', chunk => chunks.push(chunk));
|
||||||
|
|
||||||
return new Promise((res, rej) => stream.on('end', () => res(Buffer.concat(chunks))));
|
return new Promise((res, rej) => stream.on('end', () => res(Buffer.concat(chunks))));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @returns {Promise<Buffer>} */
|
function readContent(zipfile: yauzl.ZipFile, entry: yauzl.Entry): Promise<Buffer> {
|
||||||
function readContent(zipfile, entry) {
|
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
zipfile.openReadStream(entry, function(err, readStream) {
|
zipfile.openReadStream(entry, function(err, readStream) {
|
||||||
if (err) rej(err);
|
if (err) rej(err);
|
||||||
|
if (!readStream) throw new Error("Unable to read content.");
|
||||||
|
|
||||||
streamToBuffer(readStream).then(res);
|
streamToBuffer(readStream).then(res);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function readZipFile(buffer, processEntryCallback) {
|
function readZipFile(buffer: Buffer, processEntryCallback: (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => void) {
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
yauzl.fromBuffer(buffer, {lazyEntries: true, validateEntrySizes: false}, function(err, zipfile) {
|
yauzl.fromBuffer(buffer, {lazyEntries: true, validateEntrySizes: false}, function(err, zipfile) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
|
if (!zipfile) throw new Error("Unable to read zip file.");
|
||||||
|
|
||||||
zipfile.readEntry();
|
zipfile.readEntry();
|
||||||
zipfile.on("entry", entry => processEntryCallback(zipfile, entry));
|
zipfile.on("entry", entry => processEntryCallback(zipfile, entry));
|
||||||
zipfile.on("end", res);
|
zipfile.on("end", res);
|
||||||
@ -660,20 +639,19 @@ function readZipFile(buffer, processEntryCallback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveNoteType(type) {
|
function resolveNoteType(type: string | undefined): NoteType {
|
||||||
// BC for ZIPs created in Triliun 0.57 and older
|
// BC for ZIPs created in Triliun 0.57 and older
|
||||||
if (type === 'relation-map') {
|
if (type === 'relation-map') {
|
||||||
type = 'relationMap';
|
return 'relationMap';
|
||||||
} else if (type === 'note-map') {
|
} else if (type === 'note-map') {
|
||||||
type = 'noteMap';
|
return 'noteMap';
|
||||||
} else if (type === 'web-view') {
|
} else if (type === 'web-view') {
|
||||||
type = 'webView';
|
return 'webView';
|
||||||
}
|
}
|
||||||
|
|
||||||
return type || "text";
|
return "text";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export = {
|
||||||
module.exports = {
|
|
||||||
importZip
|
importZip
|
||||||
};
|
};
|
@ -1,9 +1,12 @@
|
|||||||
|
import { AttributeType } from "../../becca/entities/rows";
|
||||||
|
|
||||||
interface AttributeMeta {
|
interface AttributeMeta {
|
||||||
type: string;
|
noteId?: string;
|
||||||
|
type: AttributeType;
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
isInheritable: boolean;
|
isInheritable?: boolean;
|
||||||
position: number;
|
position?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export = AttributeMeta;
|
export = AttributeMeta;
|
||||||
|
@ -17,6 +17,7 @@ interface NoteMeta {
|
|||||||
dirFileName?: string;
|
dirFileName?: string;
|
||||||
/** this file should not be imported (e.g., HTML navigation) */
|
/** this file should not be imported (e.g., HTML navigation) */
|
||||||
noImport?: boolean;
|
noImport?: boolean;
|
||||||
|
isImportRoot?: boolean;
|
||||||
attributes?: AttributeMeta[];
|
attributes?: AttributeMeta[];
|
||||||
attachments?: AttachmentMeta[];
|
attachments?: AttachmentMeta[];
|
||||||
children?: NoteMeta[];
|
children?: NoteMeta[];
|
||||||
|
@ -167,7 +167,7 @@ interface NoteParams {
|
|||||||
/** default is false */
|
/** default is false */
|
||||||
isExpanded?: boolean;
|
isExpanded?: boolean;
|
||||||
/** default is empty string */
|
/** default is empty string */
|
||||||
prefix?: string;
|
prefix?: string | null;
|
||||||
/** default is the last existing notePosition in a parent + 10 */
|
/** default is the last existing notePosition in a parent + 10 */
|
||||||
notePosition?: number;
|
notePosition?: number;
|
||||||
dateCreated?: string;
|
dateCreated?: string;
|
||||||
@ -657,7 +657,7 @@ function saveAttachments(note: BNote, content: string) {
|
|||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveLinks(note: BNote, content: string) {
|
function saveLinks(note: BNote, content: string | Buffer) {
|
||||||
if ((note.type !== 'text' && note.type !== 'relationMap')
|
if ((note.type !== 'text' && note.type !== 'relationMap')
|
||||||
|| (note.isProtected && !protectedSessionService.isProtectedSessionAvailable())) {
|
|| (note.isProtected && !protectedSessionService.isProtectedSessionAvailable())) {
|
||||||
return {
|
return {
|
||||||
@ -669,7 +669,7 @@ function saveLinks(note: BNote, content: string) {
|
|||||||
const foundLinks: FoundLink[] = [];
|
const foundLinks: FoundLink[] = [];
|
||||||
let forceFrontendReload = false;
|
let forceFrontendReload = false;
|
||||||
|
|
||||||
if (note.type === 'text') {
|
if (note.type === 'text' && typeof content === "string") {
|
||||||
content = downloadImages(note.noteId, content);
|
content = downloadImages(note.noteId, content);
|
||||||
content = saveAttachments(note, content);
|
content = saveAttachments(note, content);
|
||||||
|
|
||||||
@ -679,7 +679,7 @@ function saveLinks(note: BNote, content: string) {
|
|||||||
|
|
||||||
({forceFrontendReload, content} = checkImageAttachments(note, content));
|
({forceFrontendReload, content} = checkImageAttachments(note, content));
|
||||||
}
|
}
|
||||||
else if (note.type === 'relationMap') {
|
else if (note.type === 'relationMap' && typeof content === "string") {
|
||||||
findRelationMapLinks(content, foundLinks);
|
findRelationMapLinks(content, foundLinks);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -874,7 +874,7 @@ function getUndeletedParentBranchIds(noteId: string, deleteId: string) {
|
|||||||
AND parentNote.isDeleted = 0`, [noteId, deleteId]);
|
AND parentNote.isDeleted = 0`, [noteId, deleteId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scanForLinks(note: BNote, content: string) {
|
function scanForLinks(note: BNote, content: string | Buffer) {
|
||||||
if (!note || !['text', 'relationMap'].includes(note.type)) {
|
if (!note || !['text', 'relationMap'].includes(note.type)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -896,7 +896,7 @@ function scanForLinks(note: BNote, content: string) {
|
|||||||
/**
|
/**
|
||||||
* Things which have to be executed after updating content, but asynchronously (separate transaction)
|
* Things which have to be executed after updating content, but asynchronously (separate transaction)
|
||||||
*/
|
*/
|
||||||
async function asyncPostProcessContent(note: BNote, content: string) {
|
async function asyncPostProcessContent(note: BNote, content: string | Buffer) {
|
||||||
if (cls.isMigrationRunning()) {
|
if (cls.isMigrationRunning()) {
|
||||||
// this is rarely needed for migrations, but can cause trouble by e.g. triggering downloads
|
// this is rarely needed for migrations, but can cause trouble by e.g. triggering downloads
|
||||||
return;
|
return;
|
||||||
|
@ -226,8 +226,8 @@ function removeTextFileExtension(filePath: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNoteTitle(filePath: string, replaceUnderscoresWithSpaces: boolean, noteMeta?: { title: string }) {
|
function getNoteTitle(filePath: string, replaceUnderscoresWithSpaces: boolean, noteMeta?: { title?: string }) {
|
||||||
if (noteMeta) {
|
if (noteMeta?.title) {
|
||||||
return noteMeta.title;
|
return noteMeta.title;
|
||||||
} else {
|
} else {
|
||||||
const basename = path.basename(removeTextFileExtension(filePath));
|
const basename = path.basename(removeTextFileExtension(filePath));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user