From fa6e70a13aef19c3eee4a3357802d822885b6817 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 28 Mar 2026 12:01:01 +0200 Subject: [PATCH] feat(standalone): get enex import to work --- .../trilium-core/src/routes/api/import.ts | 16 ++--- .../trilium-core/src/services/import/enex.ts | 59 ++++++------------- .../trilium-core/src/services/utils/index.ts | 10 +++- 3 files changed, 35 insertions(+), 50 deletions(-) diff --git a/packages/trilium-core/src/routes/api/import.ts b/packages/trilium-core/src/routes/api/import.ts index 89b3018881..15ce84860a 100644 --- a/packages/trilium-core/src/routes/api/import.ts +++ b/packages/trilium-core/src/routes/api/import.ts @@ -5,7 +5,7 @@ type ImportRequest

= Omit, "file"> & { file?: File }; import becca from "../../becca/becca.js"; import type BNote from "../../becca/entities/bnote.js"; -// import enexImportService from "../../services/import/enex.js"; +import enexImportService from "../../services/import/enex.js"; import opmlImportService from "../../services/import/opml.js"; import singleImportService from "../../services/import/single.js"; import zipImportService from "../../services/import/zip.js"; @@ -62,18 +62,18 @@ async function importNotesToBranch(req: ImportRequest<{ parentNoteId: string }>) return importResult; } } else if (extension === ".enex" && options.explodeArchives) { - throw "ENEX import is currently not supported. Please use the desktop app to import ENEX files and then sync with the server."; - // const importResult = await enexImportService.importEnex(taskContext, file, parentNote); - // if (!Array.isArray(importResult)) { - // note = importResult; - // } else { - // return importResult; - // } + const importResult = await enexImportService.importEnex(taskContext, file, parentNote); + if (!Array.isArray(importResult)) { + note = importResult; + } else { + return importResult; + } } else { note = singleImportService.importSingleFile(taskContext, file, parentNote); } } catch (e: unknown) { const [errMessage, errStack] = safeExtractMessageAndStackFromError(e); + console.warn(e); const message = `Import failed with following error: '${errMessage}'. More details might be in the logs.`; taskContext.reportError(message); diff --git a/packages/trilium-core/src/services/import/enex.ts b/packages/trilium-core/src/services/import/enex.ts index 18ab4e888d..252a7dc119 100644 --- a/packages/trilium-core/src/services/import/enex.ts +++ b/packages/trilium-core/src/services/import/enex.ts @@ -1,8 +1,6 @@ import type { AttributeType } from "@triliumnext/commons"; import { dayjs } from "@triliumnext/commons"; import sax from "sax"; -import stream from "stream"; -import { Throttle } from "stream-throttle"; import type BNote from "../../becca/entities/bnote.js"; import date_utils from "../utils/date.js"; @@ -59,8 +57,8 @@ interface Note { let note: Partial = {}; let resource: Resource; -function importEnex(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote): Promise { - const saxStream = sax.createStream(true); +function importEnex(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote): BNote { + const parser = sax.parser(true); const rootNoteTitle = file.originalname.toLowerCase().endsWith(".enex") ? file.originalname.substr(0, file.originalname.length - 5) : file.originalname; @@ -138,15 +136,14 @@ function importEnex(taskContext: TaskContext<"importNotes">, file: File, parentN } } - saxStream.on("error", (e) => { - // unhandled errors will throw, since this is a proper node event emitter. + parser.onerror = (e) => { getLog().error(`error when parsing ENEX file: ${e}`); - // clear the error - (saxStream._parser as any).error = null; - saxStream._parser.resume(); - }); + // clear the error and resume + parser.error = null; + parser.resume(); + }; - saxStream.on("text", (text) => { + parser.ontext = (text) => { const currentTag = getCurrentTag(); const previousTag = getPreviousTag(); @@ -209,13 +206,9 @@ function importEnex(taskContext: TaskContext<"importNotes">, file: File, parentN } // unknown tags are just ignored } - }); + }; - saxStream.on("attribute", (attr) => { - // an attribute. attr has "name" and "value" - }); - - saxStream.on("opentag", (tag) => { + parser.onopentag = (tag) => { path.push(tag.name); if (tag.name === "note") { @@ -235,7 +228,7 @@ function importEnex(taskContext: TaskContext<"importNotes">, file: File, parentN note.resources.push(resource); } } - }); + }; const sql = getSql(); @@ -381,38 +374,22 @@ function importEnex(taskContext: TaskContext<"importNotes">, file: File, parentN updateDates(noteEntity, utcDateCreated, utcDateModified); } - saxStream.on("closetag", (tag) => { + parser.onclosetag = (tag) => { path.pop(); if (tag === "note") { saveNote(); } - }); + }; - saxStream.on("opencdata", () => { - //console.log("opencdata"); - }); - - saxStream.on("cdata", (text) => { + parser.oncdata = (text) => { note.content += text; - }); + }; - saxStream.on("closecdata", () => { - //console.log("closecdata"); - }); + const content = typeof file.buffer === "string" ? file.buffer : new TextDecoder().decode(file.buffer); + parser.write(content).close(); - return new Promise((resolve, reject) => { - // resolve only when we parse the whole document AND saving of all notes have been finished - saxStream.on("end", () => resolve(rootNote)); - - const bufferStream = new stream.PassThrough(); - bufferStream.end(file.buffer); - - bufferStream - // rate limiting to improve responsiveness during / after import - .pipe(new Throttle({ rate: 500000 })) - .pipe(saxStream); - }); + return rootNote; } function formatDateTimeToLocalDbFormat( diff --git a/packages/trilium-core/src/services/utils/index.ts b/packages/trilium-core/src/services/utils/index.ts index 25785e2d1e..a1427becbf 100644 --- a/packages/trilium-core/src/services/utils/index.ts +++ b/packages/trilium-core/src/services/utils/index.ts @@ -199,7 +199,15 @@ export function randomSecureToken(bytes = 32) { } export function safeExtractMessageAndStackFromError(err: unknown): [errMessage: string, errStack: string | undefined] { - return (err instanceof Error) ? [err.message, err.stack] as const : ["Unknown Error", undefined] as const; + if (err instanceof Error) { + return [err.message, err.stack] as const; + } + + if (typeof err === "string") { + return [err, undefined] as const; + } + + return ["Unknown Error", undefined] as const; } export function isEmptyOrWhitespace(str: string | null | undefined) {