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 = Logger.create(__filename, electronConsole); import Elements from './elements'; import { $, $$, $$$, $$$$ } from './elements/require/q-module'; $.setDocument(document); interface WithPotentialErrorParams { taskFunc: (() => Promise), errorIndicatorAddFunc: ((element: HTMLElement) => Promise), errorContainer: HTMLElement, errorClasses?: string[], errorMessage: string, } export default class Util { static async exists(path: string): Promise { // 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 { 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 { 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); } } }