cordis/client/webapp/util.ts
2021-10-30 12:26:41 -05:00

129 lines
4.9 KiB
TypeScript

import * as fs from 'fs/promises';
import * as path from 'path';
import * as electronRemote from '@electron/remote';
const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../logger/logger';
const LOG = new Logger(__filename, electronConsole);
import Elements from './elements';
import { $, $$, $$$, $$$$ } from './elements/require/q-module';
$.setDocument(document);
interface WithPotentialErrorParams {
taskFunc: (() => Promise<void>),
errorIndicatorAddFunc: ((element: HTMLElement) => Promise<void>),
errorContainer: HTMLElement,
errorClasses?: string[],
errorMessage: string,
}
export default class Util {
static async exists(path: string): Promise<boolean> {
// Check if a file exists
try {
await fs.stat(path);
return true;
} catch (e) {
if (e.code == 'ENOENT') {
return false;
} else {
throw new Error('Error checking if file exists: ' + e.code);
}
}
}
static sanitize(name: string): string {
// Windows Version (created for Windows, most likely works cross-platform too given my research)
// Allowed Characters: Extended Unicode Charset (1-255)
// Illegal file names: CON, PRN, AUX, NUL, COM1, COM2, ..., COM9, LPT1, LPT2, ..., LPT9
// Reserved Characters: <>:"/\|?*
// Solution: Replace reserved characters with empty string (''), bad characters with '_', and append '_' to bad names
// Illegal File Names (Windows)
if ([ 'CON', 'PRN', 'AUX', 'NUL',
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9' ].indexOf(name) != -1) { // TODO: case insensitive?
name += '_';
}
// Reserved Characters
name = name.replace(/[<>:\"\/\\|?*]/g, '');
// Allowed Characters
return name.split('').map(c => c.charCodeAt(0) < 255 && c.charCodeAt(0) > 0 ? c : '_').join('');
// Much stricter whitelist version
// replace bad characters with '_'
//return name.split('').map(c => /[A-Za-z0-9-]/.exec(c) ? c : '_').join('');
}
// Checks a directory for a file with matching name, adding a number to the name
// until such file does not exist in the directory. The number will start at 2 and
// increment by one. E.g. file.txt -> file2.txt -> file3.txt
// Does not join the name with the dir
static async getAvailableFileName(dir: string, name: string): Promise<string> {
name = Util.sanitize(name);
let ext = path.extname(name);
let baseName = path.basename(name, ext);
let availableBaseName = baseName;
let tries = 1;
while (await Util.exists(path.join(dir, availableBaseName + ext))) {
availableBaseName = baseName + '-' + (++tries);
}
return availableBaseName + ext;
}
// This MUST be called before an errorContainer is removed from the document to prevent memory leaks (if it's parent element is removed, that's the big problem)
// This can also be called to remove error indicators from an error container.
static removeErrorIndicators(errorContainer: HTMLElement, extraClasses?: string[]): void {
extraClasses = extraClasses ?? [];
let querySelector = '.error-indicator' + extraClasses.map(e => '.' + e).join('');
for (let element of $$$$(errorContainer, querySelector)) {
element.parentElement?.removeChild(element); // The MutationObserver will reject the outstanding Promise
}
}
// Will return once the fetchFunc was called successfully OR the request was canceled.
// If the error indicator element is removed from the error container, this will reject
// Note: Detected using MutationObservers
// If the error container removed from the document, this could result in memory leaks
// NOTE: This should NOT be called within an element lock
static async withPotentialError(params: WithPotentialErrorParams): Promise<unknown> {
const { taskFunc, errorIndicatorAddFunc, errorContainer, errorClasses, errorMessage } = params;
return await new Promise(async (resolve, reject) => {
try {
await taskFunc();
resolve(null);
} catch (e) {
LOG.error('caught potential error', e);
let errorIndicatorElement = Elements.createErrorIndicator({
container: errorContainer,
classes: errorClasses,
message: errorMessage,
taskFunc: taskFunc,
resolveFunc: resolve,
rejectFunc: reject
});
await errorIndicatorAddFunc(errorIndicatorElement);
if (errorIndicatorElement.parentElement != errorContainer) {
if (errorIndicatorElement.parentElement) {
errorIndicatorElement.parentElement.removeChild(errorIndicatorElement);
}
LOG.error('error indicator was not added to the error container');
reject(new Error('bad errorIndicatorAddFunc'));
}
}
});
}
static async withPotentialErrorWarnOnCancel(params: WithPotentialErrorParams) {
try {
await Util.withPotentialError(params)
} catch (e) {
LOG.warn('with potential error canceled:', e);
}
}
}