'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var node_child_process = require('node:child_process'); var promises = require('node:fs/promises'); var node_os = require('node:os'); var node_path = require('node:path'); var node_process = require('node:process'); var node_util = require('node:util'); class InvalidPathError extends Error { constructor(message) { super(message); this.name = 'InvalidPathError'; // Set the prototype explicitly. Object.setPrototypeOf(this, InvalidPathError.prototype); } } class NoMatchError extends Error { constructor(message) { super(message); this.name = 'NoMatchError'; // Set the prototype explicitly. Object.setPrototypeOf(this, NoMatchError.prototype); } } /** * Tells if directory exists * * @param directoryPath - The file/folder path * @param dependencies - Dependencies container */ async function isDirectoryExisting(directoryPath, dependencies) { try { await dependencies.fsAccess(directoryPath); return Promise.resolve(true); } catch (error) { return Promise.resolve(false); } } /** * Get the first existing parent path * * @param directoryPath - The file/folder path from where we want to know disk space * @param dependencies - Dependencies container */ async function getFirstExistingParentPath(directoryPath, dependencies) { let parentDirectoryPath = dependencies.pathNormalize(directoryPath); let parentDirectoryFound = await isDirectoryExisting(parentDirectoryPath, dependencies); const FAILED_TO_FIND_EXISTING_DIRECTORY_VALUE = ''; /** * Linux max file path length is 4096 characters. * With / separators and 1 letter folder names, this gives us a max of ~2048 folders to traverse. * This is much less error prone than a while loop. */ const maxNumberOfFolders = 2048; for (let i = 0; i < maxNumberOfFolders && !parentDirectoryFound; ++i) { const newParentDirectoryPath = dependencies.pathNormalize(parentDirectoryPath + '/..'); if (parentDirectoryPath === newParentDirectoryPath) { return FAILED_TO_FIND_EXISTING_DIRECTORY_VALUE; } parentDirectoryPath = newParentDirectoryPath; parentDirectoryFound = await isDirectoryExisting(parentDirectoryPath, dependencies); } if (parentDirectoryPath !== '.') { return parentDirectoryPath; } return FAILED_TO_FIND_EXISTING_DIRECTORY_VALUE; } /** * Tell if PowerShell 3 is available based on Windows version * * Note: 6.* is Windows 7 * Note: PowerShell 3 is natively available since Windows 8 * * @param dependencies - Dependencies Injection Container */ async function hasPowerShell3(dependencies) { const major = parseInt(dependencies.release.split('.')[0], 10); if (major <= 6) { return false; } try { await dependencies.cpExecFile('where', ['powershell'], { windowsHide: true }); return true; } catch (error) { return false; } } /** * Check disk space * * @param directoryPath - The file/folder path from where we want to know disk space * @param dependencies - Dependencies container */ function checkDiskSpace(directoryPath, dependencies = { platform: node_process.platform, release: node_os.release(), fsAccess: promises.access, pathNormalize: node_path.normalize, pathSep: node_path.sep, cpExecFile: node_util.promisify(node_child_process.execFile), }) { // Note: This function contains other functions in order // to wrap them in a common context and make unit tests easier /** * Maps command output to a normalized object {diskPath, free, size} * * @param stdout - The command output * @param filter - To filter drives (only used for win32) * @param mapping - Map between column index and normalized column name * @param coefficient - The size coefficient to get bytes instead of kB */ function mapOutput(stdout, filter, mapping, coefficient) { const parsed = stdout .split('\n') // Split lines .map(line => line.trim()) // Trim all lines .filter(line => line.length !== 0) // Remove empty lines .slice(1) // Remove header .map(line => line.split(/\s+(?=[\d/])/)); // Split on spaces to get columns const filtered = parsed.filter(filter); if (filtered.length === 0) { throw new NoMatchError(); } const diskData = filtered[0]; return { diskPath: diskData[mapping.diskPath], free: parseInt(diskData[mapping.free], 10) * coefficient, size: parseInt(diskData[mapping.size], 10) * coefficient, }; } /** * Run the command and do common things between win32 and unix * * @param cmd - The command to execute * @param filter - To filter drives (only used for win32) * @param mapping - Map between column index and normalized column name * @param coefficient - The size coefficient to get bytes instead of kB */ async function check(cmd, filter, mapping, coefficient = 1) { const [file, ...args] = cmd; /* istanbul ignore if */ if (file === undefined) { return Promise.reject(new Error('cmd must contain at least one item')); } try { const { stdout } = await dependencies.cpExecFile(file, args, { windowsHide: true }); return mapOutput(stdout, filter, mapping, coefficient); } catch (error) { return Promise.reject(error); } } /** * Build the check call for win32 * * @param directoryPath - The file/folder path from where we want to know disk space */ async function checkWin32(directoryPath) { if (directoryPath.charAt(1) !== ':') { return Promise.reject(new InvalidPathError(`The following path is invalid (should be X:\\...): ${directoryPath}`)); } const powershellCmd = [ 'powershell', 'Get-CimInstance -ClassName Win32_LogicalDisk | Select-Object Caption, FreeSpace, Size', ]; const wmicCmd = [ 'wmic', 'logicaldisk', 'get', 'size,freespace,caption', ]; const cmd = await hasPowerShell3(dependencies) ? powershellCmd : wmicCmd; return check(cmd, driveData => { // Only get the drive which match the path const driveLetter = driveData[0]; return directoryPath.toUpperCase().startsWith(driveLetter.toUpperCase()); }, { diskPath: 0, free: 1, size: 2, }); } /** * Build the check call for unix * * @param directoryPath - The file/folder path from where we want to know disk space */ async function checkUnix(directoryPath) { if (!dependencies.pathNormalize(directoryPath).startsWith(dependencies.pathSep)) { return Promise.reject(new InvalidPathError(`The following path is invalid (should start by ${dependencies.pathSep}): ${directoryPath}`)); } const pathToCheck = await getFirstExistingParentPath(directoryPath, dependencies); return check([ 'df', '-Pk', '--', pathToCheck, ], () => true, // We should only get one line, so we did not need to filter { diskPath: 5, free: 3, size: 1, }, 1024); } // Call the right check depending on the OS if (dependencies.platform === 'win32') { return checkWin32(directoryPath); } return checkUnix(directoryPath); } exports.InvalidPathError = InvalidPathError; exports.NoMatchError = NoMatchError; exports.default = checkDiskSpace; exports.getFirstExistingParentPath = getFirstExistingParentPath;