164 lines
5.9 KiB
TypeScript
164 lines
5.9 KiB
TypeScript
import * as electronRemote from '@electron/remote';
|
|
const electronConsole = electronRemote.getGlobal('console') as Console;
|
|
import Logger from '../../logger/logger';
|
|
const LOG = Logger.create(__filename, electronConsole);
|
|
|
|
import * as fs from 'fs/promises';
|
|
import * as path from 'path';
|
|
|
|
import * as socketio from 'socket.io-client';
|
|
|
|
import Q from './q-module';
|
|
import createErrorIndicator from './elements/error-indicator';
|
|
|
|
|
|
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(q: Q, errorContainer: HTMLElement, extraClasses?: string[]): void {
|
|
extraClasses = extraClasses ?? [];
|
|
let querySelector = '.error-indicator' + extraClasses.map(e => '.' + e).join('');
|
|
for (let element of q.$$$$(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(q: Q, 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.debug('params', { params });
|
|
LOG.error('caught potential error', e);
|
|
let errorIndicatorElement = createErrorIndicator(q, {
|
|
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(q: Q, params: WithPotentialErrorParams) {
|
|
try {
|
|
await Util.withPotentialError(q, params)
|
|
} catch (e) {
|
|
LOG.warn('with potential error canceled:', e);
|
|
}
|
|
}
|
|
|
|
// This function is a promise for error stack tracking purposes.
|
|
// this function expects the last argument of args to be a callback(err, serverData)
|
|
static socketEmitTimeout(
|
|
socket: socketio.Socket,
|
|
timeoutMS: number,
|
|
endpoint: string,
|
|
...args: any[]
|
|
): Promise<void> {
|
|
return new Promise<void>((resolve) => {
|
|
let socketArgs = args.slice(0, args.length - 1);
|
|
let respond = args[args.length - 1];
|
|
|
|
if (typeof respond !== 'function') {
|
|
throw new Error('no response function provided');
|
|
}
|
|
|
|
let cutoff = false;
|
|
let timeout = setTimeout(() => {
|
|
cutoff = true;
|
|
respond('emit timeout');
|
|
resolve();
|
|
}, timeoutMS);
|
|
|
|
socket.emit(endpoint, ...socketArgs, (...respondArgs: any[]) => {
|
|
if (cutoff) {
|
|
LOG.warn(`@${endpoint}/[${LOG.inspect(socketArgs)}]: result came after timeout`);
|
|
return;
|
|
}
|
|
clearTimeout(timeout);
|
|
respond(...respondArgs);
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
}
|