129 lines
4.9 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
}
|