From e06c562d491344b859a334d6cf3fc57bb9cd1bac Mon Sep 17 00:00:00 2001 From: Junxiao Shi Date: Sun, 9 Jun 2024 01:51:30 +0000 Subject: [PATCH] Recognize recursive option in mkdir and mkdirSync resolves #75 --- src/emulation/promises.ts | 31 ++++++++++++++++++++++++++----- src/emulation/sync.ts | 34 +++++++++++++++++++++++++++------- tests/fs/directory.test.ts | 27 ++++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 13 deletions(-) diff --git a/src/emulation/promises.ts b/src/emulation/promises.ts index 60e1a8609..477226897 100644 --- a/src/emulation/promises.ts +++ b/src/emulation/promises.ts @@ -9,7 +9,7 @@ import type { ReadableStreamController } from 'stream/web'; import { Errno, ErrnoError } from '../error.js'; import type { File } from '../file.js'; import { isAppendable, isExclusive, isReadable, isTruncating, isWriteable, parseFlag } from '../file.js'; -import type { FileContents } from '../filesystem.js'; +import type { FileContents, FileSystem } from '../filesystem.js'; import { BigIntStats, FileType, type Stats } from '../stats.js'; import { normalizeMode, normalizeOptions, normalizePath, normalizeTime } from '../utils.js'; import * as constants from './constants.js'; @@ -656,13 +656,34 @@ export async function mkdir(path: fs.PathLike, options: fs.MakeDirectoryOptions export async function mkdir(path: fs.PathLike, options?: fs.Mode | (fs.MakeDirectoryOptions & { recursive?: false | undefined }) | null): Promise; export async function mkdir(path: fs.PathLike, options?: fs.Mode | fs.MakeDirectoryOptions | null): Promise; export async function mkdir(path: fs.PathLike, options?: fs.Mode | fs.MakeDirectoryOptions | null): Promise { + options = typeof options === 'object' ? options : { mode: options }; + const mode = normalizeMode(options?.mode, 0o777); + path = normalizePath(path); path = (await exists(path)) ? await realpath(path) : path; const { fs, path: resolved } = resolveMount(path); - try { - await fs.mkdir(resolved, normalizeMode(typeof options == 'object' ? options?.mode : options, 0o777), cred); - } catch (e) { - throw fixError(e as Error, { [resolved]: path }); + const errorPaths: Record = { [resolved]: path }; + + const mkdirSingle = async (dir: string) => { + try { + await fs.mkdir(dir, mode, cred); + } catch (e) { + throw fixError(e as Error, errorPaths); + } + }; + + if (options?.recursive) { + const dirs: string[] = []; + for (let dir = resolved, origDir = path; !(await fs.exists(dir, cred)); dir = dirname(dir), origDir = dirname(origDir)) { + dirs.unshift(dir); + errorPaths[dir] = origDir; + } + for (const dir of dirs) { + await mkdirSingle(dir); + } + return dirs[0]; + } else { + return mkdirSingle(resolved); } } mkdir satisfies typeof promises.mkdir; diff --git a/src/emulation/sync.ts b/src/emulation/sync.ts index 9fb1d6e32..0a768047d 100644 --- a/src/emulation/sync.ts +++ b/src/emulation/sync.ts @@ -481,14 +481,34 @@ export function mkdirSync(path: fs.PathLike, options: fs.MakeDirectoryOptions & export function mkdirSync(path: fs.PathLike, options?: fs.Mode | (fs.MakeDirectoryOptions & { recursive?: false }) | null): void; export function mkdirSync(path: fs.PathLike, options?: fs.Mode | fs.MakeDirectoryOptions | null): string | undefined; export function mkdirSync(path: fs.PathLike, options?: fs.Mode | fs.MakeDirectoryOptions | null): string | undefined | void { - const mode: fs.Mode = normalizeMode(typeof options == 'number' || typeof options == 'string' ? options : options?.mode, 0o777); - const recursive = typeof options == 'object' && options?.recursive; + options = typeof options === 'object' ? options : { mode: options }; + const mode = normalizeMode(options?.mode, 0o777); + path = normalizePath(path); - const { fs, path: resolved } = resolveMount(existsSync(path) ? realpathSync(path) : path); - try { - return fs.mkdirSync(resolved, mode, cred); - } catch (e) { - throw fixError(e as Error, { [resolved]: path }); + path = existsSync(path) ? realpathSync(path) : path; + const { fs, path: resolved } = resolveMount(path); + const errorPaths: Record = { [resolved]: path }; + + const mkdirSingle = (dir: string) => { + try { + fs.mkdirSync(dir, mode, cred); + } catch (e) { + throw fixError(e as Error, errorPaths); + } + }; + + if (options?.recursive) { + const dirs: string[] = []; + for (let dir = resolved, origDir = path; !fs.existsSync(dir, cred); dir = dirname(dir), origDir = dirname(origDir)) { + dirs.unshift(dir); + errorPaths[dir] = origDir; + } + for (const dir of dirs) { + mkdirSingle(dir); + } + return dirs[0]; + } else { + return mkdirSingle(resolved); } } mkdirSync satisfies typeof fs.mkdirSync; diff --git a/tests/fs/directory.test.ts b/tests/fs/directory.test.ts index b8ca7a0a3..b8de72889 100644 --- a/tests/fs/directory.test.ts +++ b/tests/fs/directory.test.ts @@ -3,7 +3,8 @@ import { fs } from '../common.js'; describe('Directory', () => { test('mkdir', async () => { await fs.promises.mkdir('/one', 0o755); - expect(await fs.promises.exists('/one')).toBe(true); + await expect(fs.promises.exists('/one')).resolves.toBe(true); + await expect(fs.promises.mkdir('/one', 0o755)).rejects.toThrow(/EEXIST/); }); test('mkdirSync', () => fs.mkdirSync('/two', 0o000)); @@ -17,6 +18,30 @@ describe('Directory', () => { expect(await fs.promises.exists('/nested/dir')).toBe(false); }); + test('mkdir, recursive', async () => { + await expect(fs.promises.mkdir('/recursiveP/A/B', { recursive: true, mode: 0o755 })).resolves.toBe('/recursiveP'); + await expect(fs.promises.mkdir('/recursiveP/A/B/C/D', { recursive: true, mode: 0o777 })).resolves.toBe('/recursiveP/A/B/C'); + await expect(fs.promises.mkdir('/recursiveP/A/B/C/D', { recursive: true, mode: 0o700 })).resolves.toBeUndefined(); + + await expect(fs.promises.stat('/recursiveP')).resolves.toMatchObject({ mode: fs.constants.S_IFDIR | 0o755 }); + await expect(fs.promises.stat('/recursiveP/A')).resolves.toMatchObject({ mode: fs.constants.S_IFDIR | 0o755 }); + await expect(fs.promises.stat('/recursiveP/A/B')).resolves.toMatchObject({ mode: fs.constants.S_IFDIR | 0o755 }); + await expect(fs.promises.stat('/recursiveP/A/B/C')).resolves.toMatchObject({ mode: fs.constants.S_IFDIR | 0o777 }); + await expect(fs.promises.stat('/recursiveP/A/B/C/D')).resolves.toMatchObject({ mode: fs.constants.S_IFDIR | 0o777 }); + }); + + test('mkdirSync, recursive', () => { + expect(fs.mkdirSync('/recursiveS/A/B', { recursive: true, mode: 0o755 })).toBe('/recursiveS'); + expect(fs.mkdirSync('/recursiveS/A/B/C/D', { recursive: true, mode: 0o777 })).toBe('/recursiveS/A/B/C'); + expect(fs.mkdirSync('/recursiveS/A/B/C/D', { recursive: true, mode: 0o700 })).toBeUndefined(); + + expect(fs.statSync('/recursiveS')).toMatchObject({ mode: fs.constants.S_IFDIR | 0o755 }); + expect(fs.statSync('/recursiveS/A')).toMatchObject({ mode: fs.constants.S_IFDIR | 0o755 }); + expect(fs.statSync('/recursiveS/A/B')).toMatchObject({ mode: fs.constants.S_IFDIR | 0o755 }); + expect(fs.statSync('/recursiveS/A/B/C')).toMatchObject({ mode: fs.constants.S_IFDIR | 0o777 }); + expect(fs.statSync('/recursiveS/A/B/C/D')).toMatchObject({ mode: fs.constants.S_IFDIR | 0o777 }); + }); + test('readdirSync without permission', () => { try { fs.readdirSync('/two');