mirror of
https://github.com/zadam/trilium.git
synced 2026-05-06 20:17:01 +02:00
fix(import/enex): attachments were imported as files (closs #9473)
This commit is contained in:
90
apps/server/src/services/import/enex.spec.ts
Normal file
90
apps/server/src/services/import/enex.spec.ts
Normal 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&attachmentId=${txt!.attachmentId}"`);
|
||||
expect(content).toContain(`class="reference-link" href="#root/${test1!.noteId}?viewMode=attachments&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);
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 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>
|
||||
Reference in New Issue
Block a user