Notes/src/services/utils.ts

369 lines
9.2 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 env from "./env.js";
import { dirname, join } from "path";
const randtoken = generator({source: 'crypto'});
2018-04-02 20:30:00 -04:00
function newEntityId() {
2018-02-11 00:18:59 -05:00
return randomString(12);
}
2024-02-17 10:56:27 +02:00
function randomString(length: number): string {
return randtoken.generate(length);
}
function randomSecureToken(bytes = 32) {
2017-10-29 11:22:41 -04:00
return crypto.randomBytes(bytes).toString('base64');
}
2024-02-16 21:38:09 +02:00
function md5(content: crypto.BinaryLike) {
return crypto.createHash('md5').update(content).digest('hex');
}
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
const base64Hash = crypto.createHash('sha512').update(content).digest('base64');
// we don't want such + and / in the IDs
const kindaBase62Hash = base64Hash
2023-12-30 00:34:46 +01:00
.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
}
function toBase64(plainText: string | Buffer) {
return Buffer.from(plainText).toString('base64');
}
2024-02-16 21:38:09 +02:00
function fromBase64(encodedText: string) {
return Buffer.from(encodedText, 'base64');
}
2024-02-16 21:38:09 +02:00
function hmac(secret: any, value: any) {
const hmac = crypto.createHmac('sha256', Buffer.from(secret.toString(), 'ascii'));
2017-10-29 14:55:48 -04:00
hmac.update(value.toString());
return hmac.digest('base64');
}
function isElectron() {
return !!process.versions['electron'];
}
2024-02-16 21:38:09 +02:00
function hash(text: string) {
text = text.normalize();
2017-11-21 22:11:27 -05:00
return crypto.createHash('sha1').update(text).digest('base64');
}
2024-02-16 21:38:09 +02:00
function isEmptyOrWhitespace(str: string) {
return str === null || str.match(/^ *$/) !== null;
}
2024-02-16 21:38:09 +02:00
function sanitizeSqlIdentifier(str: string) {
2019-12-24 16:00:31 +01:00
return str.replace(/[^A-Za-z0-9_]/g, "");
}
2024-02-16 21:38:09 +02:00
function escapeHtml(str: string) {
return escape(str);
}
2024-02-16 21:38:09 +02:00
function unescapeHtml(str: string) {
return unescape(str);
}
2024-02-16 21:38:09 +02:00
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;
}
2024-02-16 21:38:09 +02:00
function stripTags(text: string) {
return text.replace(/<(?:.|\n)*?>/gm, '');
}
2024-02-16 21:38:09 +02:00
function union<T extends string | number | symbol>(a: T[], b: T[]): T[] {
2024-02-18 20:29:23 +02:00
const obj: Record<T, T> = {} as Record<T, T>; // TODO: unsafe?
for (let i = a.length-1; i >= 0; i--) {
obj[a[i]] = a[i];
}
for (let i = b.length-1; i >= 0; i--) {
obj[b[i]] = b[i];
}
2024-02-16 21:38:09 +02:00
const res: T[] = [];
for (const k in obj) {
if (obj.hasOwnProperty(k)) { // <-- optional
res.push(obj[k]);
}
}
return res;
}
2024-02-16 21:38:09 +02:00
function escapeRegExp(str: string) {
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}
2024-07-18 23:26:21 +03:00
async function crash() {
if (isElectron()) {
2024-07-18 23:26:21 +03:00
(await import("electron")).app.exit(1);
} else {
process.exit(1);
}
}
2024-02-16 21:38:09 +02:00
function sanitizeFilenameForHeader(filename: string) {
let sanitizedFilename = sanitize(filename);
if (sanitizedFilename.trim().length === 0) {
sanitizedFilename = "file";
}
2023-03-16 18:34:39 +01:00
return encodeURIComponent(sanitizedFilename);
}
2024-02-16 21:38:09 +02:00
function getContentDisposition(filename: string) {
const sanitizedFilename = sanitizeFilenameForHeader(filename);
return `file; filename="${sanitizedFilename}"; filename*=UTF-8''${sanitizedFilename}`;
}
const STRING_MIME_TYPES = [
"application/javascript",
"application/x-javascript",
2021-01-30 15:59:59 +01:00
"application/json",
"application/x-sql",
"image/svg+xml"
];
function isStringNote(type: string | null, mime: string) {
2020-07-01 22:42:59 +02:00
// render and book are string note in the sense that they are expected to contain empty string
return (type && ["text", "code", "relationMap", "search", "render", "book", "mermaid", "canvas"].includes(type))
|| mime.startsWith('text/')
|| STRING_MIME_TYPES.includes(mime);
}
2024-02-16 21:38:09 +02:00
function quoteRegex(url: string) {
return url.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
}
2024-02-16 21:38:09 +02:00
function replaceAll(string: string, replaceWhat: string, replaceWith: string) {
const quotedReplaceWhat = quoteRegex(replaceWhat);
return string.replace(new RegExp(quotedReplaceWhat, "g"), replaceWith);
}
function formatDownloadTitle(fileName: string, type: string | null, mime: string) {
2023-05-03 10:23:20 +02:00
if (!fileName) {
fileName = "untitled";
}
2023-05-03 10:23:20 +02:00
fileName = sanitize(fileName);
2021-04-24 11:39:59 +02:00
if (type === 'text') {
2023-05-03 10:23:20 +02:00
return `${fileName}.html`;
} else if (type && ['relationMap', 'canvas', 'search'].includes(type)) {
2023-05-03 10:23:20 +02:00
return `${fileName}.json`;
} else {
if (!mime) {
2023-05-03 10:23:20 +02:00
return fileName;
}
mime = mime.toLowerCase();
2023-05-03 10:23:20 +02:00
const filenameLc = fileName.toLowerCase();
const extensions = mimeTypes.extensions[mime];
if (!extensions || extensions.length === 0) {
2023-05-03 10:23:20 +02:00
return fileName;
}
2020-04-02 22:55:11 +02:00
for (const ext of extensions) {
if (filenameLc.endsWith(`.${ext}`)) {
2023-05-03 10:23:20 +02:00
return fileName;
}
}
if (mime === 'application/octet-stream') {
// we didn't find any good guess for this one, it will be better to just return
2023-06-29 23:32:19 +02:00
// the current name without a fake extension. It's possible that the title still preserves the correct
// extension too
2023-05-03 10:23:20 +02:00
return fileName;
}
2023-05-03 10:23:20 +02:00
return `${fileName}.${extensions[0]}`;
}
2020-04-02 22:55:11 +02:00
}
2024-02-16 21:38:09 +02:00
function removeTextFileExtension(filePath: string) {
const extension = path.extname(filePath).toLowerCase();
switch (extension) {
case ".md":
case ".markdown":
case ".html":
case ".htm":
return filePath.substr(0, filePath.length - extension.length);
default:
return filePath;
}
}
2024-04-03 22:46:14 +03:00
function getNoteTitle(filePath: string, replaceUnderscoresWithSpaces: boolean, noteMeta?: { title?: string }) {
if (noteMeta?.title) {
return noteMeta.title;
} else {
const basename = path.basename(removeTextFileExtension(filePath));
if (replaceUnderscoresWithSpaces) {
return basename.replace(/_/g, ' ').trim();
}
return basename;
}
}
2024-02-18 13:10:51 +02:00
function timeLimit<T>(promise: Promise<T>, limitMs: number, errorMessage?: string): Promise<T> {
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 error = new Error(errorMessage || `Process exceeded time limit ${limitMs}`);
2020-07-28 23:29:12 +02:00
return new Promise((res, rej) => {
let resolved = false;
2020-06-13 22:34:15 +02:00
promise.then(result => {
resolved = true;
2020-06-13 22:34:15 +02:00
res(result);
2020-07-01 21:33:52 +02:00
})
.catch(error => rej(error));
setTimeout(() => {
if (!resolved) {
2020-07-28 23:29:12 +02:00
rej(error);
}
}, limitMs);
});
}
2024-02-16 21:38:09 +02:00
interface DeferredPromise<T> extends Promise<T> {
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void
}
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
})();
}
2024-02-16 21:38:09 +02:00
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, "");
}
2024-02-16 21:38:09 +02:00
function normalize(str: string) {
return removeDiacritic(str).toLowerCase();
}
2024-02-16 21:38:09 +02:00
function toMap<T extends Record<string, any>>(list: T[], key: keyof T): Record<string, T> {
const map: Record<string, T> = {};
for (const el of list) {
map[el[key]] = el;
}
return map;
}
2024-02-16 21:38:09 +02:00
function isString(x: any) {
return Object.prototype.toString.call(x) === "[object String]";
}
/**
* 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() && !env.isDev()) {
return process.resourcesPath;
} else {
return join(dirname(fileURLToPath(import.meta.url)), "..", "..");
}
}
export default {
randomSecureToken,
2017-10-28 12:12:20 -04:00
randomString,
md5,
2018-04-02 20:30:00 -04:00
newEntityId,
toBase64,
2017-10-29 14:55:48 -04:00
fromBase64,
hmac,
2017-11-05 21:56:42 -05:00
isElectron,
hash,
2017-12-16 00:05:37 -05:00
isEmptyOrWhitespace,
sanitizeSqlIdentifier,
escapeHtml,
2018-03-04 12:06:35 -05:00
unescapeHtml,
toObject,
stripTags,
union,
escapeRegExp,
crash,
getContentDisposition,
2020-04-02 22:55:11 +02:00
isStringNote,
quoteRegex,
replaceAll,
getNoteTitle,
removeTextFileExtension,
formatDownloadTitle,
2020-06-20 21:42:41 +02:00
timeLimit,
deferred,
removeDiacritic,
normalize,
2023-03-15 22:44:08 +01:00
hashedBlobId,
toMap,
isString
};