feat(standalone): get enex import to work

This commit is contained in:
Elian Doran
2026-03-28 12:01:01 +02:00
parent 9b6c7966de
commit fa6e70a13a
3 changed files with 35 additions and 50 deletions

View File

@@ -5,7 +5,7 @@ type ImportRequest<P> = Omit<Request<P>, "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);

View File

@@ -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<Note> = {};
let resource: Resource;
function importEnex(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote): Promise<BNote> {
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(

View File

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