diff --git a/apps/server/src/services/import/single.ts b/apps/server/src/services/import/single.ts index 5abb6008a..7200d17d5 100644 --- a/apps/server/src/services/import/single.ts +++ b/apps/server/src/services/import/single.ts @@ -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); diff --git a/apps/server/src/services/import/zip.ts b/apps/server/src/services/import/zip.ts index c1ac90b91..2d2251cb3 100644 --- a/apps/server/src/services/import/zip.ts +++ b/apps/server/src/services/import/zip.ts @@ -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>/gi, (match, text) => { if (noteTitle.trim() === text.trim()) { return ""; // remove whole H1 tag - } else { - return `

${text}

`; - } + } + return `

${text}

`; + }); 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 { export function readContent(zipfile: yauzl.ZipFile, entry: yauzl.Entry): Promise { 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) { return new Promise((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>" ]; for (const tag of tagsToRemove) { - let re = new RegExp(tag, "gi"); + const re = new RegExp(tag, "gi"); content = content.replace(re, ""); } diff --git a/apps/server/src/services/utils.spec.ts b/apps/server/src/services/utils.spec.ts index d767d2d25..1a69d7dd1 100644 --- a/apps/server/src/services/utils.spec.ts +++ b/apps/server/src/services/utils.spec.ts @@ -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); }); }); diff --git a/apps/server/src/services/utils.ts b/apps/server/src/services/utils.ts index 370f9297f..a97b84a6c 100644 --- a/apps/server/src/services/utils.ts +++ b/apps/server/src/services/utils.ts @@ -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,