feat(import/zip): remove extension from title for PDF imports

This commit is contained in:
Elian Doran 2026-01-16 16:25:15 +02:00
parent 3a0880fcd6
commit f6924d7fda
No known key found for this signature in database
4 changed files with 79 additions and 86 deletions

View File

@ -1,5 +1,4 @@
import type { NoteType } from "@triliumnext/commons";
import { extname } from "path";
import type BNote from "../../becca/entities/bnote.js";
import imageService from "../../services/image.js";
@ -55,13 +54,14 @@ function importImage(file: File, parentNote: BNote, taskContext: TaskContext<"im
function importFile(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote) {
const originalName = file.originalname;
const mime = mimeService.getMime(originalName) || file.mimetype;
const { note } = noteService.createNewNote({
parentNoteId: parentNote.noteId,
title: removeFileExtension(originalName),
title: getNoteTitle(originalName, mime === "application/pdf"),
content: file.buffer,
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
type: "file",
mime: mimeService.getMime(originalName) || file.mimetype
mime
});
note.addLabel("originalFileName", originalName);
@ -71,17 +71,6 @@ function importFile(taskContext: TaskContext<"importNotes">, file: File, parentN
return note;
}
function removeFileExtension(filename: string) {
const extension = extname(filename).toLowerCase();
switch (extension) {
case ".pdf":
return filename.substring(0, filename.length - extension.length);
default:
return filename;
}
}
function importCodeNote(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote) {
const title = getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces);
const content = processStringOrBuffer(file.buffer);

View File

@ -1,26 +1,27 @@
"use strict";
import BAttribute from "../../becca/entities/battribute.js";
import { removeTextFileExtension, newEntityId, getNoteTitle, processStringOrBuffer, unescapeHtml } from "../../services/utils.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
import attributeService from "../../services/attributes.js";
import BBranch from "../../becca/entities/bbranch.js";
import { ALLOWED_NOTE_TYPES, type NoteType } from "@triliumnext/commons";
import path from "path";
import protectedSessionService from "../protected_session.js";
import mimeService from "./mime.js";
import treeService from "../tree.js";
import type { Stream } from "stream";
import yauzl from "yauzl";
import htmlSanitizer from "../html_sanitizer.js";
import becca from "../../becca/becca.js";
import BAttachment from "../../becca/entities/battachment.js";
import markdownService from "./markdown.js";
import type TaskContext from "../task_context.js";
import BAttribute from "../../becca/entities/battribute.js";
import BBranch from "../../becca/entities/bbranch.js";
import type BNote from "../../becca/entities/bnote.js";
import type NoteMeta from "../meta/note_meta.js";
import attributeService from "../../services/attributes.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
import { getNoteTitle, newEntityId, processStringOrBuffer, removeFileExtension, unescapeHtml } from "../../services/utils.js";
import htmlSanitizer from "../html_sanitizer.js";
import type AttributeMeta from "../meta/attribute_meta.js";
import type { Stream } from "stream";
import { ALLOWED_NOTE_TYPES, type NoteType } from "@triliumnext/commons";
import type NoteMeta from "../meta/note_meta.js";
import protectedSessionService from "../protected_session.js";
import type TaskContext from "../task_context.js";
import treeService from "../tree.js";
import markdownService from "./markdown.js";
import mimeService from "./mime.js";
interface MetaFile {
files: NoteMeta[];
@ -108,7 +109,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
dataFileName: ""
};
let parent: NoteMeta | undefined = undefined;
let parent: NoteMeta | undefined;
for (let segment of pathSegments) {
if (!cursor?.children?.length) {
@ -161,7 +162,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
// in case we lack metadata, we treat e.g. "Programming.html" and "Programming" as the same note
// (one data file, the other directory for children)
const filePathNoExt = removeTextFileExtension(filePath);
const filePathNoExt = removeFileExtension(filePath);
if (filePathNoExt in createdPaths) {
return createdPaths[filePathNoExt];
@ -241,10 +242,10 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
}
const { note } = noteService.createNewNote({
parentNoteId: parentNoteId,
parentNoteId,
title: noteTitle || "",
content: "",
noteId: noteId,
noteId,
type: resolveNoteType(noteMeta?.type),
mime: noteMeta ? noteMeta.mime : "text/html",
prefix: noteMeta?.prefix || "",
@ -294,12 +295,12 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
attachmentId: getNewAttachmentId(attachmentMeta.attachmentId),
noteId: getNewNoteId(noteMeta.noteId)
};
} else {
// don't check for noteMeta since it's not mandatory for notes
return {
noteId: getNoteId(noteMeta, absUrl)
};
}
}
// don't check for noteMeta since it's not mandatory for notes
return {
noteId: getNoteId(noteMeta, absUrl)
};
}
function processTextNoteContent(content: string, noteTitle: string, filePath: string, noteMeta?: NoteMeta) {
@ -312,9 +313,9 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
content = content.replace(/<h1>([^<]*)<\/h1>/gi, (match, text) => {
if (noteTitle.trim() === text.trim()) {
return ""; // remove whole H1 tag
} else {
return `<h2>${text}</h2>`;
}
}
return `<h2>${text}</h2>`;
});
if (taskContext.data?.safeImport) {
@ -347,9 +348,9 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
return `src="api/attachments/${target.attachmentId}/image/${path.basename(url)}"`;
} else if (target.noteId) {
return `src="api/images/${target.noteId}/${path.basename(url)}"`;
} else {
return match;
}
}
return match;
});
content = content.replace(/href="([^"]*)"/g, (match, url) => {
@ -373,9 +374,9 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
return `href="#root/${target.noteId}?viewMode=attachments&attachmentId=${target.attachmentId}"`;
} else if (target.noteId) {
return `href="#root/${target.noteId}"`;
} else {
return match;
}
}
return match;
});
if (noteMeta) {
@ -525,9 +526,9 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
}
({ note } = noteService.createNewNote({
parentNoteId: parentNoteId,
parentNoteId,
title: noteTitle || "",
content: content,
content,
noteId,
type,
mime,
@ -536,7 +537,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
// root notePosition should be ignored since it relates to the original document
// now import root should be placed after existing notes into new parent
notePosition: noteMeta && firstNote ? noteMeta.notePosition : undefined,
isProtected: isProtected
isProtected
}));
createdNoteIds.add(note.noteId);
@ -648,7 +649,7 @@ function streamToBuffer(stream: Stream): Promise<Buffer> {
export function readContent(zipfile: yauzl.ZipFile, entry: yauzl.Entry): Promise<Buffer> {
return new Promise((res, rej) => {
zipfile.openReadStream(entry, function (err, readStream) {
zipfile.openReadStream(entry, (err, readStream) => {
if (err) rej(err);
if (!readStream) throw new Error("Unable to read content.");
@ -659,7 +660,7 @@ export function readContent(zipfile: yauzl.ZipFile, entry: yauzl.Entry): Promise
export function readZipFile(buffer: Buffer, processEntryCallback: (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => Promise<void>) {
return new Promise<void>((res, rej) => {
yauzl.fromBuffer(buffer, { lazyEntries: true, validateEntrySizes: false }, function (err, zipfile) {
yauzl.fromBuffer(buffer, { lazyEntries: true, validateEntrySizes: false }, (err, zipfile) => {
if (err) rej(err);
if (!zipfile) throw new Error("Unable to read zip file.");
@ -691,9 +692,9 @@ function resolveNoteType(type: string | undefined): NoteType {
if (type && (ALLOWED_NOTE_TYPES as readonly string[]).includes(type)) {
return type as NoteType;
} else {
return "text";
}
}
return "text";
}
export function removeTriliumTags(content: string) {
@ -702,7 +703,7 @@ export function removeTriliumTags(content: string) {
"<title data-trilium-title>([^<]*)<\/title>"
];
for (const tag of tagsToRemove) {
let re = new RegExp(tag, "gi");
const re = new RegExp(tag, "gi");
content = content.replace(re, "");
}

View File

@ -1,4 +1,5 @@
import { describe, it, expect } from "vitest";
import { describe, expect,it } from "vitest";
import utils from "./utils.js";
type TestCase<T extends (...args: any) => any> = [desc: string, fnParams: Parameters<T>, expected: ReturnType<T>];
@ -120,7 +121,7 @@ describe("#toObject", () => {
{ testPropA: "keyA", testPropB: "valueA" },
{ testPropA: "keyB", testPropB: "valueB" }
];
const fn: TestListFn = (testListEntry: TestListEntry) => [ testListEntry.testPropA + "_fn", testListEntry.testPropB + "_fn" ];
const fn: TestListFn = (testListEntry: TestListEntry) => [ `${testListEntry.testPropA }_fn`, `${testListEntry.testPropB }_fn` ];
const result = utils.toObject(testList, fn);
expect(result).toStrictEqual({
@ -240,8 +241,8 @@ describe.todo("#quoteRegex", () => {});
describe.todo("#replaceAll", () => {});
describe("#removeTextFileExtension", () => {
const testCases: TestCase<typeof utils.removeTextFileExtension>[] = [
describe("#removeFileExtension", () => {
const testCases: TestCase<typeof utils.removeFileExtension>[] = [
[ "w/ 'test.md' it should strip '.md'", [ "test.md" ], "test" ],
[ "w/ 'test.markdown' it should strip '.markdown'", [ "test.markdown" ], "test" ],
[ "w/ 'test.html' it should strip '.html'", [ "test.html" ], "test" ],
@ -252,7 +253,7 @@ describe("#removeTextFileExtension", () => {
testCases.forEach((testCase) => {
const [ desc, fnParams, expected ] = testCase;
it(desc, () => {
const result = utils.removeTextFileExtension(...fnParams);
const result = utils.removeFileExtension(...fnParams);
expect(result).toStrictEqual(expected);
});
});

View File

@ -1,18 +1,19 @@
"use strict";
import chardet from "chardet";
import stripBom from "strip-bom";
import crypto from "crypto";
import { generator } from "rand-token";
import unescape from "unescape";
import escape from "escape-html";
import sanitize from "sanitize-filename";
import mimeTypes from "mime-types";
import path from "path";
import type NoteMeta from "./meta/note_meta.js";
import log from "./log.js";
import { t } from "i18next";
import mimeTypes from "mime-types";
import { release as osRelease } from "os";
import path from "path";
import { generator } from "rand-token";
import sanitize from "sanitize-filename";
import stripBom from "strip-bom";
import unescape from "unescape";
import log from "./log.js";
import type NoteMeta from "./meta/note_meta.js";
const osVersion = osRelease().split('.').map(Number);
@ -204,7 +205,7 @@ export function formatDownloadTitle(fileName: string, type: string | null, mime:
return `${fileNameBase}${getExtension()}`;
}
export function removeTextFileExtension(filePath: string) {
export function removeFileExtension(filePath: string) {
const extension = path.extname(filePath).toLowerCase();
switch (extension) {
@ -216,6 +217,7 @@ export function removeTextFileExtension(filePath: string) {
case ".excalidraw":
case ".mermaid":
case ".mmd":
case ".pdf":
return filePath.substring(0, filePath.length - extension.length);
default:
return filePath;
@ -226,7 +228,7 @@ export function getNoteTitle(filePath: string, replaceUnderscoresWithSpaces: boo
const trimmedNoteMeta = noteMeta?.title?.trim();
if (trimmedNoteMeta) return trimmedNoteMeta;
const basename = path.basename(removeTextFileExtension(filePath));
const basename = path.basename(removeFileExtension(filePath));
return replaceUnderscoresWithSpaces ? basename.replace(/_/g, " ").trim() : basename;
}
@ -467,28 +469,28 @@ export function normalizeCustomHandlerPattern(pattern: string | null | undefined
// If already ends with slash, create both versions
if (basePattern.endsWith('/')) {
const withoutSlash = basePattern.slice(0, -1) + '$';
const withoutSlash = `${basePattern.slice(0, -1) }$`;
const withSlash = pattern;
return [withoutSlash, withSlash];
} else {
// Add optional trailing slash
const withSlash = basePattern + '/?$';
return [withSlash];
}
// Add optional trailing slash
const withSlash = `${basePattern }/?$`;
return [withSlash];
}
// For patterns without $, add both versions
if (pattern.endsWith('/')) {
const withoutSlash = pattern.slice(0, -1);
return [withoutSlash, pattern];
} else {
const withSlash = pattern + '/';
return [pattern, withSlash];
}
const withSlash = `${pattern }/`;
return [pattern, withSlash];
}
export function formatUtcTime(time: string) {
return time.replace("T", " ").substring(0, 19)
return time.replace("T", " ").substring(0, 19);
}
// TODO: Deduplicate with client utils
@ -501,9 +503,9 @@ export function formatSize(size: number | null | undefined) {
if (size < 1024) {
return `${size} KiB`;
} else {
return `${Math.round(size / 102.4) / 10} MiB`;
}
return `${Math.round(size / 102.4) / 10} MiB`;
}
function slugify(text: string) {
@ -544,7 +546,7 @@ export default {
randomSecureToken,
randomString,
removeDiacritic,
removeTextFileExtension,
removeFileExtension,
replaceAll,
safeExtractMessageAndStackFromError,
sanitizeSqlIdentifier,