From 8d79fd8851e815ccd38cac812a36dad9df37944a Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Wed, 22 May 2019 13:22:50 -0400 Subject: [PATCH 1/5] Create an optimistic mkdirp --- packages/io/__tests__/io.test.ts | 23 +++--------- packages/io/src/io-util.ts | 43 +++++++++++++++++++++++ packages/io/src/io.ts | 60 +------------------------------- 3 files changed, 49 insertions(+), 77 deletions(-) diff --git a/packages/io/__tests__/io.test.ts b/packages/io/__tests__/io.test.ts index ddaedded65..8bbf0d931f 100644 --- a/packages/io/__tests__/io.test.ts +++ b/packages/io/__tests__/io.test.ts @@ -3,6 +3,7 @@ import {promises as fs} from 'fs' import * as os from 'os' import * as path from 'path' import * as io from '../src/io' +import * as ioUtil from '../src/io-util' describe('cp', () => { it('copies file with no flags', async () => { @@ -688,18 +689,6 @@ describe('mkdirP', () => { expect(worked).toBe(false) }) - it('fails if mkdirP with empty path', async () => { - let worked: boolean - try { - await io.mkdirP('') - worked = true - } catch (err) { - worked = false - } - - expect(worked).toBe(false) - }) - it('fails if mkdirP with conflicting file path', async () => { const testPath = path.join(getTestTemp(), 'mkdirP_conflicting_file_path') await io.mkdirP(getTestTemp()) @@ -807,14 +796,12 @@ describe('mkdirP', () => { '9', '10' ) - process.env['TEST_MKDIRP_FAILSAFE'] = '10' + + expect.assertions(1) + try { - await io.mkdirP(testPath) - throw new Error('directory should not have been created') + await ioUtil.mkdirP(testPath, 10) } catch (err) { - delete process.env['TEST_MKDIRP_FAILSAFE'] - - // ENOENT is expected, all other errors are not expect(err.code).toBe('ENOENT') } }) diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index 6aaa622f47..d02a8a3881 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -54,6 +54,49 @@ export function isRooted(p: string): boolean { return p.startsWith('/') } +/** + * Recursively create a directory at `fsPath`. + * + * This implementation is optimistic, meaning it attempts to create the full + * path first, and backs up the path stack from there. + * + * @param fsPath The path to create + * @param maxDepth The maximum recursion depth + * @param depth The current recursion depth + */ +export async function mkdirP( + fsPath: string, + maxDepth: number = 1000, + depth: number = 1 +): Promise { + fsPath = path.resolve(fsPath) + + if (depth >= maxDepth) return mkdir(fsPath) + + try { + await mkdir(fsPath) + } catch (err) { + switch (err.code) { + case 'ENOENT': { + await mkdirP(path.dirname(fsPath), maxDepth, depth + 1) + await mkdirP(fsPath, maxDepth, depth + 1) + break + } + default: { + let stats: fs.Stats + + try { + stats = await stat(fsPath) + } catch (err2) { + throw err + } + + if (!stats.isDirectory()) throw err + } + } + } +} + /** * Best effort attempt to determine whether a file exists and is executable. * @param filePath file path to check diff --git a/packages/io/src/io.ts b/packages/io/src/io.ts index a1f44bea04..25811854f5 100644 --- a/packages/io/src/io.ts +++ b/packages/io/src/io.ts @@ -102,65 +102,7 @@ export async function rmRF(inputPath: string): Promise { * @returns Promise */ export async function mkdirP(fsPath: string): Promise { - if (!fsPath) { - throw new Error('Parameter p is required') - } - - // build a stack of directories to create - const stack: string[] = [] - let testDir: string = fsPath - - // eslint-disable-next-line no-constant-condition - while (true) { - // validate the loop is not out of control - if (stack.length >= (process.env['TEST_MKDIRP_FAILSAFE'] || 1000)) { - // let the framework throw - await ioUtil.mkdir(fsPath) - return - } - - let stats: fs.Stats - try { - stats = await ioUtil.stat(testDir) - } catch (err) { - if (err.code === 'ENOENT') { - // validate the directory is not the drive root - const parentDir = path.dirname(testDir) - if (testDir === parentDir) { - throw new Error( - `Unable to create directory '${fsPath}'. Root directory does not exist: '${testDir}'` - ) - } - - // push the dir and test the parent - stack.push(testDir) - testDir = parentDir - continue - } else if (err.code === 'UNKNOWN') { - throw new Error( - `Unable to create directory '${fsPath}'. Unable to verify the directory exists: '${testDir}'. If directory is a file share, please verify the share name is correct, the share is online, and the current process has permission to access the share.` - ) - } else { - throw err - } - } - - if (!stats.isDirectory()) { - throw new Error( - `Unable to create directory '${fsPath}'. Conflicting file exists: '${testDir}'` - ) - } - - // testDir exists - break - } - - // create each directory - let dir = stack.pop() - while (dir != null) { - await ioUtil.mkdir(dir) - dir = stack.pop() - } + await ioUtil.mkdirP(fsPath) } /** From d19a10b72ef3ee7a2afe6c4b8f9519ea5dfd45a7 Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Wed, 22 May 2019 14:56:15 -0400 Subject: [PATCH 2/5] Update io-util.ts --- packages/io/src/io-util.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index d02a8a3881..d83cb6d3a7 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -74,13 +74,12 @@ export async function mkdirP( if (depth >= maxDepth) return mkdir(fsPath) try { - await mkdir(fsPath) + return mkdir(fsPath) } catch (err) { switch (err.code) { case 'ENOENT': { await mkdirP(path.dirname(fsPath), maxDepth, depth + 1) - await mkdirP(fsPath, maxDepth, depth + 1) - break + return mkdir(fsPath) } default: { let stats: fs.Stats From 9c3517ba7267e7d368ec2c434591e334f6929648 Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Wed, 22 May 2019 15:05:48 -0400 Subject: [PATCH 3/5] Update io-util.ts --- packages/io/src/io-util.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index d83cb6d3a7..e1c7b2e5d0 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -1,3 +1,4 @@ +import {ok} from 'assert' import * as fs from 'fs' import * as path from 'path' @@ -69,6 +70,8 @@ export async function mkdirP( maxDepth: number = 1000, depth: number = 1 ): Promise { + ok(fsPath, 'a path argument must be provided') + fsPath = path.resolve(fsPath) if (depth >= maxDepth) return mkdir(fsPath) From d73dd7504e804333d4f791adf58981f029eb910c Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Wed, 22 May 2019 15:13:24 -0400 Subject: [PATCH 4/5] Update io.test.ts --- packages/io/__tests__/io.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/io/__tests__/io.test.ts b/packages/io/__tests__/io.test.ts index 8bbf0d931f..4b18311da5 100644 --- a/packages/io/__tests__/io.test.ts +++ b/packages/io/__tests__/io.test.ts @@ -659,6 +659,16 @@ describe('mkdirP', () => { await io.rmRF(getTestTemp()) }) + it('fails when called with an empty path', async () => { + expect.assertions(1) + + try { + await io.mkdirP('') + } catch(err) { + expect(err.message).toEqual('a path argument must be provided') + } + }) + it('creates folder', async () => { const testPath = path.join(getTestTemp(), 'mkdirTest') await io.mkdirP(testPath) From acb4428a9da1363202c2b14a954894bf99cf9525 Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Wed, 22 May 2019 15:51:12 -0400 Subject: [PATCH 5/5] Fix tests for mkdirP --- packages/io/__tests__/io.test.ts | 2 +- packages/io/src/io-util.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/io/__tests__/io.test.ts b/packages/io/__tests__/io.test.ts index 4b18311da5..9521825051 100644 --- a/packages/io/__tests__/io.test.ts +++ b/packages/io/__tests__/io.test.ts @@ -664,7 +664,7 @@ describe('mkdirP', () => { try { await io.mkdirP('') - } catch(err) { + } catch (err) { expect(err.message).toEqual('a path argument must be provided') } }) diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index e1c7b2e5d0..d5d4e6777a 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -77,12 +77,14 @@ export async function mkdirP( if (depth >= maxDepth) return mkdir(fsPath) try { - return mkdir(fsPath) + await mkdir(fsPath) + return } catch (err) { switch (err.code) { case 'ENOENT': { await mkdirP(path.dirname(fsPath), maxDepth, depth + 1) - return mkdir(fsPath) + await mkdir(fsPath) + return } default: { let stats: fs.Stats