import { Dirent, promises } from 'fs' import { join, isAbsolute, dirname } from 'path' import { promisify } from 'util' import isError from './is-error' const sleep = promisify(setTimeout) const unlinkPath = async (p: string, isDir = false, t = 1): Promise => { try { if (isDir) { await promises.rmdir(p) } else { await promises.unlink(p) } } catch (e) { const code = isError(e) && e.code if ( (code === 'EBUSY' || code === 'ENOTEMPTY' || code === 'EPERM' || code === 'EMFILE') && t < 3 ) { await sleep(t * 100) return unlinkPath(p, isDir, t++) } if (code === 'ENOENT') { return } throw e } } /** * Recursively delete directory contents * @param {string} dir Directory to delete the contents of * @param {RegExp} [exclude] Exclude based on relative file path * @param {string} [previousPath] Ensures that parameter dir exists, this is not passed recursively * @returns Promise void */ export async function recursiveDelete( dir: string, exclude?: RegExp, previousPath: string = '' ): Promise { let result try { result = await promises.readdir(dir, { withFileTypes: true }) } catch (e) { if (isError(e) && e.code === 'ENOENT') { return } throw e } await Promise.all( result.map(async (part: Dirent) => { const absolutePath = join(dir, part.name) // readdir does not follow symbolic links // if part is a symbolic link, follow it using stat let isDirectory = part.isDirectory() const isSymlink = part.isSymbolicLink() if (isSymlink) { const linkPath = await promises.readlink(absolutePath) try { const stats = await promises.stat( isAbsolute(linkPath) ? linkPath : join(dirname(absolutePath), linkPath) ) isDirectory = stats.isDirectory() } catch (_) {} } const pp = join(previousPath, part.name) const isNotExcluded = !exclude || !exclude.test(pp) if (isNotExcluded) { if (isDirectory) { await recursiveDelete(absolutePath, exclude, pp) } return unlinkPath(absolutePath, !isSymlink && isDirectory) } }) ) }