server-ts: Port services/import/zip

This commit is contained in:
Elian Doran 2024-04-03 22:46:14 +03:00
parent 764d251b0a
commit 53d4873c1f
No known key found for this signature in database
7 changed files with 136 additions and 158 deletions

View File

@ -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;

View File

@ -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))) {

View File

@ -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
}; };

View File

@ -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;

View File

@ -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[];

View File

@ -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;

View File

@ -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));