Notes/src/services/utils.ts

331 lines
9.3 KiB
TypeScript
Raw Normal View History

2017-10-21 21:10:33 -04:00
"use strict";
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 { fileURLToPath } from "url";
import { dirname, join } from "path";
import type NoteMeta from "./meta/note_meta.js";
2025-01-09 18:07:02 +02:00
const randtoken = generator({ source: "crypto" });
export const isMac = process.platform === "darwin";
export const isWindows = process.platform === "win32";
export const isElectron = !!process.versions["electron"];
export const isDev = !!(process.env.TRILIUM_ENV && process.env.TRILIUM_ENV === "dev");
export function newEntityId() {
2018-02-11 00:18:59 -05:00
return randomString(12);
}
export function randomString(length: number): string {
return randtoken.generate(length);
}
export function randomSecureToken(bytes = 32) {
2025-01-09 18:07:02 +02:00
return crypto.randomBytes(bytes).toString("base64");
}
export function md5(content: crypto.BinaryLike) {
2025-01-09 18:07:02 +02:00
return crypto.createHash("md5").update(content).digest("hex");
}
export function hashedBlobId(content: string | Buffer) {
2023-09-18 23:45:00 +02:00
if (content === null || content === undefined) {
content = "";
}
2023-03-15 22:44:08 +01:00
// sha512 is faster than sha256
2025-01-09 18:07:02 +02:00
const base64Hash = crypto.createHash("sha512").update(content).digest("base64");
2023-03-15 22:44:08 +01:00
// we don't want such + and / in the IDs
2025-01-09 18:07:02 +02:00
const kindaBase62Hash = base64Hash.replaceAll("+", "X").replaceAll("/", "Y");
// 20 characters of base62 gives us ~120 bit of entropy which is plenty enough
return kindaBase62Hash.substr(0, 20);
2023-03-15 22:44:08 +01:00
}
export function toBase64(plainText: string | Buffer) {
2025-01-09 18:07:02 +02:00
return Buffer.from(plainText).toString("base64");
}
export function fromBase64(encodedText: string) {
2025-01-09 18:07:02 +02:00
return Buffer.from(encodedText, "base64");
}
export function hmac(secret: any, value: any) {
2025-01-09 18:07:02 +02:00
const hmac = crypto.createHmac("sha256", Buffer.from(secret.toString(), "ascii"));
2017-10-29 14:55:48 -04:00
hmac.update(value.toString());
2025-01-09 18:07:02 +02:00
return hmac.digest("base64");
2017-10-29 14:55:48 -04:00
}
export function hash(text: string) {
text = text.normalize();
2025-01-09 18:07:02 +02:00
return crypto.createHash("sha1").update(text).digest("base64");
2017-11-21 22:11:27 -05:00
}
export function isEmptyOrWhitespace(str: string | null | undefined) {
if (!str) return true;
return str.match(/^ *$/) !== null;
}
export function sanitizeSqlIdentifier(str: string) {
2019-12-24 16:00:31 +01:00
return str.replace(/[^A-Za-z0-9_]/g, "");
}
export function escapeHtml(str: string) {
return escape(str);
}
export function unescapeHtml(str: string) {
return unescape(str);
}
export function toObject<T, K extends string | number | symbol, V>(array: T[], fn: (item: T) => [K, V]): Record<K, V> {
2024-02-18 20:29:23 +02:00
const obj: Record<K, V> = {} as Record<K, V>; // TODO: unsafe?
2018-03-04 12:06:35 -05:00
for (const item of array) {
const ret = fn(item);
obj[ret[0]] = ret[1];
}
return obj;
}
export function stripTags(text: string) {
2025-01-09 18:07:02 +02:00
return text.replace(/<(?:.|\n)*?>/gm, "");
}
export function escapeRegExp(str: string) {
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}
export async function crash() {
if (isElectron) {
2024-07-18 23:26:21 +03:00
(await import("electron")).app.exit(1);
} else {
process.exit(1);
}
}
export function getContentDisposition(filename: string) {
const sanitizedFilename = sanitize(filename).trim() || "file";
const uriEncodedFilename = encodeURIComponent(sanitizedFilename);
return `file; filename="${uriEncodedFilename}"; filename*=UTF-8''${uriEncodedFilename}`;
}
// render and book are string note in the sense that they are expected to contain empty string
const STRING_NOTE_TYPES = new Set(["text", "code", "relationMap", "search", "render", "book", "mermaid", "canvas"]);
2025-01-09 18:07:02 +02:00
const STRING_MIME_TYPES = new Set(["application/javascript", "application/x-javascript", "application/json", "application/x-sql", "image/svg+xml"]);
export function isStringNote(type: string | undefined, mime: string) {
return (type && STRING_NOTE_TYPES.has(type)) || mime.startsWith("text/") || STRING_MIME_TYPES.has(mime);
}
export function quoteRegex(url: string) {
2025-01-09 18:07:02 +02:00
return url.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&");
}
export function replaceAll(string: string, replaceWhat: string, replaceWith: string) {
const quotedReplaceWhat = quoteRegex(replaceWhat);
return string.replace(new RegExp(quotedReplaceWhat, "g"), replaceWith);
}
export function formatDownloadTitle(fileName: string, type: string | null, mime: string) {
2025-01-09 18:07:02 +02:00
const fileNameBase = !fileName ? "untitled" : sanitize(fileName);
2025-01-09 18:07:02 +02:00
const getExtension = () => {
if (type === "text") return ".html";
if (type === "relationMap" || type === "canvas" || type === "search") return ".json";
if (!mime) return "";
2021-04-24 11:39:59 +02:00
2025-01-09 18:07:02 +02:00
const mimeLc = mime.toLowerCase();
2025-01-09 18:07:02 +02:00
// better to just return the current name without a fake extension
// it's possible that the title still preserves the correct extension anyways
if (mimeLc === "application/octet-stream") return "";
2025-01-09 18:07:02 +02:00
// if fileName has an extension matching the mime already - reuse it
const mimeTypeFromFileName = mimeTypes.lookup(fileName);
if (mimeTypeFromFileName === mimeLc) return "";
2020-04-02 22:55:11 +02:00
2025-01-09 18:07:02 +02:00
// as last resort try to get extension from mimeType
const extensions = mimeTypes.extension(mime);
return extensions ? `.${extensions}` : "";
};
2025-01-09 18:07:02 +02:00
return `${fileNameBase}${getExtension()}`;
2020-04-02 22:55:11 +02:00
}
export function removeTextFileExtension(filePath: string) {
const extension = path.extname(filePath).toLowerCase();
switch (extension) {
case ".md":
case ".markdown":
case ".html":
case ".htm":
return filePath.substring(0, filePath.length - extension.length);
default:
return filePath;
}
}
export function getNoteTitle(filePath: string, replaceUnderscoresWithSpaces: boolean, noteMeta?: NoteMeta) {
2024-04-03 22:46:14 +03:00
if (noteMeta?.title) {
return noteMeta.title;
} else {
const basename = path.basename(removeTextFileExtension(filePath));
if (replaceUnderscoresWithSpaces) {
2025-01-09 18:07:02 +02:00
return basename.replace(/_/g, " ").trim();
}
return basename;
}
}
export function timeLimit<T>(promise: Promise<T>, limitMs: number, errorMessage?: string): Promise<T> {
// TriliumNextTODO: since TS avoids this from ever happening do we need this check?
2025-01-09 18:07:02 +02:00
if (!promise || !promise.then) {
// it's not actually a promise
return promise;
}
2020-07-28 23:29:12 +02:00
// better stack trace if created outside of promise
const errorTimeLimit = new Error(errorMessage || `Process exceeded time limit ${limitMs}`);
2020-07-28 23:29:12 +02:00
return new Promise((res, rej) => {
let resolved = false;
2025-01-09 18:07:02 +02:00
promise
.then((result) => {
resolved = true;
2025-01-09 18:07:02 +02:00
res(result);
})
.catch((error) => rej(error));
setTimeout(() => {
if (!resolved) {
rej(errorTimeLimit);
}
}, limitMs);
});
}
2024-02-16 21:38:09 +02:00
interface DeferredPromise<T> extends Promise<T> {
2025-01-09 18:07:02 +02:00
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
2024-02-16 21:38:09 +02:00
}
export function deferred<T>(): DeferredPromise<T> {
2020-06-20 21:42:41 +02:00
return (() => {
2024-02-16 21:38:09 +02:00
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: any) => void;
2020-06-20 21:42:41 +02:00
2024-02-16 21:38:09 +02:00
let promise = new Promise<T>((res, rej) => {
2020-06-20 21:42:41 +02:00
resolve = res;
reject = rej;
2024-02-16 21:38:09 +02:00
}) as DeferredPromise<T>;
2020-06-20 21:42:41 +02:00
promise.resolve = resolve;
promise.reject = reject;
2024-02-16 21:38:09 +02:00
return promise as DeferredPromise<T>;
2020-06-20 21:42:41 +02:00
})();
}
export function removeDiacritic(str: string) {
2022-12-30 21:00:42 +01:00
if (!str) {
return "";
}
2023-08-25 13:01:53 +08:00
str = str.toString();
return str.normalize("NFD").replace(/\p{Diacritic}/gu, "");
}
export function normalize(str: string) {
return removeDiacritic(str).toLowerCase();
}
export function toMap<T extends Record<string, any>>(list: T[], key: keyof T): Record<string, T> {
2024-02-16 21:38:09 +02:00
const map: Record<string, T> = {};
for (const el of list) {
map[el[key]] = el;
}
return map;
}
// try to turn 'true' and 'false' strings from process.env variables into boolean values or undefined
export function envToBoolean(val: string | undefined) {
if (val === undefined || typeof val !== "string") return undefined;
const valLc = val.toLowerCase().trim();
if (valLc === "true") return true;
if (valLc === "false") return false;
return undefined;
}
/**
* Returns the directory for resources. On Electron builds this corresponds to the `resources` subdirectory inside the distributable package.
* On development builds, this simply refers to the root directory of the application.
*
* @returns the resource dir.
*/
export function getResourceDir() {
if (isElectron && !isDev) {
return process.resourcesPath;
} else {
return join(dirname(fileURLToPath(import.meta.url)), "..", "..");
}
}
export default {
crash,
deferred,
envToBoolean,
escapeHtml,
escapeRegExp,
formatDownloadTitle,
2017-10-29 14:55:48 -04:00
fromBase64,
getContentDisposition,
getNoteTitle,
getResourceDir,
hash,
hashedBlobId,
hmac,
isDev,
2017-11-05 21:56:42 -05:00
isElectron,
2017-12-16 00:05:37 -05:00
isEmptyOrWhitespace,
isMac,
2020-04-02 22:55:11 +02:00
isStringNote,
isWindows,
md5,
newEntityId,
normalize,
quoteRegex,
randomSecureToken,
randomString,
removeDiacritic,
removeTextFileExtension,
replaceAll,
sanitizeSqlIdentifier,
stripTags,
2020-06-20 21:42:41 +02:00
timeLimit,
toBase64,
toMap,
toObject,
unescapeHtml
};