fix(import/enex): attachments were imported as files (closs #9473)

This commit is contained in:
Elian Doran
2026-04-17 18:15:52 +03:00
parent 9565c398f8
commit 9b1b0c5574
3 changed files with 132 additions and 32 deletions

View File

@@ -0,0 +1,90 @@
import fs from "fs";
import { default as path, dirname } from "path";
import { fileURLToPath } from "url";
import { beforeAll, describe, expect, it } from "vitest";
import becca from "../../becca/becca.js";
import type BNote from "../../becca/entities/bnote.js";
import cls from "../cls.js";
import sql_init from "../sql_init.js";
import TaskContext from "../task_context.js";
import enex from "./enex.js";
const scriptDir = dirname(fileURLToPath(import.meta.url));
async function testImport(fileName: string) {
const sample = fs.readFileSync(path.join(scriptDir, "samples", fileName));
const taskContext = TaskContext.getInstance("import-enex", "importNotes", {});
return new Promise<{ importedNote: BNote; rootNote: BNote }>((resolve, reject) => {
cls.init(async () => {
const rootNote = becca.getNote("root");
if (!rootNote) {
expect(rootNote).toBeTruthy();
return;
}
const importedNote = await enex.importEnex(taskContext, {
originalname: fileName,
mimetype: "application/enex+xml",
buffer: sample
}, rootNote as BNote);
resolve({
importedNote,
rootNote
});
});
});
}
describe("importEnex", () => {
beforeAll(async () => {
sql_init.initializeDb();
await sql_init.dbReady;
});
it("imports non-image resources as attachments instead of child notes", async () => {
const { importedNote } = await testImport("File with attachments.enex");
// The root import note should contain the individual notes as children
const test1 = importedNote.getChildNotes().find(n => n.title === "TEST1");
expect(test1).toBeTruthy();
// Non-image resources should be attachments, not child notes
const childNotes = test1!.getChildNotes();
expect(childNotes).toHaveLength(0);
// Should have two file attachments
const attachments = test1!.getAttachmentsByRole("file");
expect(attachments).toHaveLength(2);
const txt = attachments.find(a => a.title === "attachments1.txt");
expect(txt).toBeTruthy();
expect(txt!.mime).toBe("text/plain");
expect(txt!.getContent().toString()).toBe("111");
const bin = attachments.find(a => a.title === "attachments2");
expect(bin).toBeTruthy();
expect(bin!.mime).toBe("application/octet-stream");
expect(bin!.getContent().toString()).toBe("222");
// The note content should contain reference links to the attachments
const content = test1!.getContent().toString();
expect(content).toContain(`class="reference-link" href="#root/${test1!.noteId}?viewMode=attachments&amp;attachmentId=${txt!.attachmentId}"`);
expect(content).toContain(`class="reference-link" href="#root/${test1!.noteId}?viewMode=attachments&amp;attachmentId=${bin!.attachmentId}"`);
});
it("imports notes without attachments normally", async () => {
const { importedNote } = await testImport("File with attachments.enex");
const test2 = importedNote.getChildNotes().find(n => n.title === "TEST2");
expect(test2).toBeTruthy();
expect(test2!.getChildNotes()).toHaveLength(0);
expect(test2!.getAttachmentsByRole("file")).toHaveLength(0);
const test3 = importedNote.getChildNotes().find(n => n.title === "TEST3");
expect(test3).toBeTruthy();
expect(test3!.getChildNotes()).toHaveLength(0);
expect(test3!.getAttachmentsByRole("file")).toHaveLength(0);
});
}, 60_000);

View File

@@ -1,20 +1,21 @@
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 log from "../log.js";
import { md5, escapeHtml, fromBase64 } from "../utils.js";
import date_utils from "../date_utils.js";
import sql from "../sql.js";
import noteService from "../notes.js";
import imageService from "../image.js";
import protectedSessionService from "../protected_session.js";
import htmlSanitizer from "../html_sanitizer.js";
import sanitizeAttributeName from "../sanitize_attribute_name.js";
import type TaskContext from "../task_context.js";
import type BNote from "../../becca/entities/bnote.js";
import date_utils from "../date_utils.js";
import htmlSanitizer from "../html_sanitizer.js";
import imageService from "../image.js";
import log from "../log.js";
import noteService from "../notes.js";
import protectedSessionService from "../protected_session.js";
import sanitizeAttributeName from "../sanitize_attribute_name.js";
import sql from "../sql.js";
import type TaskContext from "../task_context.js";
import { escapeHtml, fromBase64,md5 } from "../utils.js";
import type { File } from "./common.js";
import type { AttributeType } from "@triliumnext/commons";
/**
* date format is e.g. 20181121T193703Z or 2013-04-14T16:19:00.000Z (Mac evernote, see #3496)
@@ -25,7 +26,7 @@ function parseDate(text: string) {
text = text.replace(/[-:]/g, "");
// insert - and : to convert it to trilium format
text = text.substr(0, 4) + "-" + text.substr(4, 2) + "-" + text.substr(6, 2) + " " + text.substr(9, 2) + ":" + text.substr(11, 2) + ":" + text.substr(13, 2) + ".000Z";
text = `${text.substr(0, 4) }-${ text.substr(4, 2) }-${ text.substr(6, 2) } ${ text.substr(9, 2) }:${ text.substr(11, 2) }:${ text.substr(13, 2) }.000Z`;
return text;
}
@@ -318,27 +319,17 @@ function importEnex(taskContext: TaskContext<"importNotes">, file: File, parentN
resource.mime = resource.mime || "application/octet-stream";
const createFileNote = () => {
const resourceNote = noteService.createNewNote({
parentNoteId: noteEntity.noteId,
const createFileAttachment = () => {
const attachment = noteEntity.saveAttachment({
role: "file",
mime: resource.mime || "application/octet-stream",
title: resource.title,
content: resource.content ?? "",
type: "file",
mime: resource.mime,
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
}).note;
content: resource.content ?? ""
});
for (const attr of resource.attributes) {
resourceNote.addAttribute(attr.type, attr.name, attr.value);
}
const attachmentLink = `<a class="reference-link" href="#root/${noteEntity.noteId}?viewMode=attachments&attachmentId=${attachment.attachmentId}">${escapeHtml(resource.title)}</a>`;
updateDates(resourceNote, utcDateCreated, utcDateModified);
taskContext.increaseProgressCount();
const resourceLink = `<a href="#root/${resourceNote.noteId}">${escapeHtml(resource.title)}</a>`;
content = (content || "").replace(mediaRegex, resourceLink);
content = (content || "").replace(mediaRegex, attachmentLink);
};
if (resource.mime && resource.mime.startsWith("image/")) {
@@ -360,10 +351,10 @@ function importEnex(taskContext: TaskContext<"importNotes">, file: File, parentN
}
} catch (e: any) {
log.error(`error when saving image from ENEX file: ${e.message}`);
createFileNote();
createFileAttachment();
}
} else {
createFileNote();
createFileAttachment();
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE en-export SYSTEM "http://xml.evernote.com/pub/evernote-export2.dtd">
<en-export export-date="20260417T081355Z" application="Evernote/Windows" version="6.x">
<note><title>TEST1</title><content><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
<en-note><div>TXT</div><div><en-media hash="698d51a19d8a121ce581499d7b701668" type="text/plain"/><en-media hash="bcbe3365e6ac95ea2c0343a2395834dd" type="application/octet-stream"/><br/></div></en-note>]]></content><created>20260417T081156Z</created><updated>20260417T081307Z</updated><note-attributes><source>desktop.win</source><source-application>evernote.win32</source-application></note-attributes><resource><data encoding="base64">
MTEx
</data><mime>text/plain</mime><resource-attributes><source-url>file://D:\Users\leon\Desktop\attachments1.txt</source-url><file-name>attachments1.txt</file-name><attachment>true</attachment></resource-attributes></resource><resource><data encoding="base64">
MjIy
</data><mime>application/octet-stream</mime><resource-attributes><source-url>file://D:\Users\leon\Desktop\attachments2</source-url><file-name>attachments2</file-name><attachment>true</attachment></resource-attributes></resource></note><note><title>TEST2</title><content><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
<en-note><div>NO&nbsp;attachments</div></en-note>]]></content><created>20260417T081317Z</created><updated>20260417T081345Z</updated><note-attributes><source>desktop.win</source><source-application>evernote.win32</source-application></note-attributes></note><note><title>TEST3</title><content><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
<en-note>
111
</en-note>]]></content><created>20260417T081337Z</created><updated>20260417T081340Z</updated><note-attributes><source>desktop.win</source><source-application>evernote.win32</source-application></note-attributes></note></en-export>