Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions lib/internal/vfs/file_system.js
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,11 @@ class VirtualFileSystem {
*/
mkdirSync(dirPath, options) {
const providerPath = this.#toProviderPath(dirPath);
return this[kProvider].mkdirSync(providerPath, options);
const created = this[kProvider].mkdirSync(providerPath, options);
// With `recursive: true` the provider returns the first directory it
// created as a provider-relative path, which has to be mapped back to a
// path the caller can use. Anything else resolves to `undefined`.
return created === undefined ? undefined : this.#toMountedPath(created);
}

/**
Expand Down Expand Up @@ -1122,7 +1126,8 @@ class VirtualFileSystem {

async mkdir(dirPath, options) {
const providerPath = toProviderPath(dirPath);
return provider.mkdir(providerPath, options);
const created = await provider.mkdir(providerPath, options);
return created === undefined ? undefined : toMountedPath(created);
},

async rmdir(dirPath) {
Expand Down
11 changes: 9 additions & 2 deletions lib/internal/vfs/providers/real.js
Original file line number Diff line number Diff line change
Expand Up @@ -378,14 +378,21 @@ class RealFSProvider extends VirtualProvider {
return fs.promises.readdir(realPath, options);
}

// With `recursive: true` the underlying fs returns the first directory that
// was created as a real filesystem path. It has to be mapped back into the
// provider's namespace so the root path is never leaked to the caller.
mkdirSync(vfsPath, options) {
const realPath = this.#resolvePath(vfsPath);
return fs.mkdirSync(realPath, options);
const created = fs.mkdirSync(realPath, options);
if (created === undefined) return undefined;
return this.#resolvedToVfsPath(created, vfsPath, 'mkdir');
}

async mkdir(vfsPath, options) {
const realPath = this.#resolvePath(vfsPath);
return fs.promises.mkdir(realPath, options);
const created = await fs.promises.mkdir(realPath, options);
if (created === undefined) return undefined;
return this.#resolvedToVfsPath(created, vfsPath, 'mkdir');
}

rmdirSync(vfsPath) {
Expand Down
14 changes: 14 additions & 0 deletions test/parallel/test-vfs-fs-mkdir-callback.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ function mounted() {
}));
}

// Recursive mkdir (cb) reports the first created dir below the mount point
{
const { myVfs, mountPoint } = mounted();
fs.mkdir(path.join(mountPoint, 'src/cb-a/b/c'), { recursive: true },
common.mustSucceed((created) => {
assert.strictEqual(created, path.join(mountPoint, 'src/cb-a'));
assert.strictEqual(
fs.statSync(path.join(mountPoint, 'src/cb-a/b/c')).isDirectory(),
true,
);
myVfs.unmount();
}));
}

// rmdir (cb)
{
const { myVfs, mountPoint } = mounted();
Expand Down
31 changes: 29 additions & 2 deletions test/parallel/test-vfs-fs-mkdirSync.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
// fs.mkdirSync dispatches to VFS, including the `recursive: true` form.

require('../common');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const vfs = require('node:vfs');

tmpdir.refresh();

const mountPoint = path.resolve('/tmp/vfs-mkdirSync-' + process.pid);
const myVfs = vfs.create();
myVfs.mkdirSync('/src', { recursive: true });
Expand All @@ -20,12 +23,36 @@ assert.strictEqual(
fs.statSync(path.join(mountPoint, 'src/d1')).isDirectory(), true,
);

// Recursive mkdir creates intermediate directories and returns the first one
// Recursive mkdir creates intermediate directories and returns the first one,
// as a path below the mount point rather than a VFS-internal path.
const created = fs.mkdirSync(path.join(mountPoint, 'src/a/b/c'),
{ recursive: true });
assert.ok(created !== undefined);
assert.strictEqual(created, path.join(mountPoint, 'src/a'));
assert.strictEqual(
fs.statSync(path.join(mountPoint, 'src/a/b/c')).isDirectory(), true,
);

// Recursive mkdir with nothing left to create returns undefined.
assert.strictEqual(
fs.mkdirSync(path.join(mountPoint, 'src/a/b'), { recursive: true }),
undefined,
);

myVfs.unmount();

// The same holds for a mounted RealFSProvider, whose root must not leak into
// the returned path.
{
const root = path.join(tmpdir.path, 'vfs-mkdirSync-real');
fs.mkdirSync(root, { recursive: true });
const realMountPoint = path.resolve('/tmp/vfs-mkdirSync-real-' + process.pid);
const realVfs = vfs.create(new vfs.RealFSProvider(root));
realVfs.mount(realMountPoint);

const realCreated = fs.mkdirSync(path.join(realMountPoint, 'x/y/z'),
{ recursive: true });
assert.strictEqual(realCreated, path.join(realMountPoint, 'x'));
assert.strictEqual(fs.existsSync(path.join(root, 'x/y/z')), true);

realVfs.unmount();
}
5 changes: 4 additions & 1 deletion test/parallel/test-vfs-fs-promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,11 @@ const vfs = require('node:vfs');
await fsp.appendFile(p('src/pw.txt'), ' more');
assert.strictEqual(fs.readFileSync(p('src/pw.txt'), 'utf8'), 'pdata more');

await fsp.mkdir(p('src/pd'));
assert.strictEqual(await fsp.mkdir(p('src/pd')), undefined);
await fsp.rmdir(p('src/pd'));
// Recursive mkdir reports the first created directory below the mount point.
assert.strictEqual(await fsp.mkdir(p('src/pr/sub'), { recursive: true }),
p('src/pr'));
await fsp.rm(p('src/pw.txt'));
assert.strictEqual(fs.existsSync(p('src/pw.txt')), false);

Expand Down
6 changes: 5 additions & 1 deletion test/parallel/test-vfs-real-provider-promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ const myVfs = vfs.create(new vfs.RealFSProvider(root));
{ code: 'ENOENT' });

// mkdir / readdir / rmdir
await myVfs.promises.mkdir('/d/sub', { recursive: true });
// Recursive mkdir reports the first created directory as a VFS path.
assert.strictEqual(await myVfs.promises.mkdir('/d/sub', { recursive: true }),
'/d');
assert.strictEqual(await myVfs.promises.mkdir('/d/sub', { recursive: true }),
undefined);
const entries = await myVfs.promises.readdir('/d');
assert.deepStrictEqual(entries.sort(), ['sub']);
await myVfs.promises.rmdir('/d/sub');
Expand Down
16 changes: 14 additions & 2 deletions test/parallel/test-vfs-real-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,25 @@ fs.mkdirSync(testDir, { recursive: true });
// mkdir / rmdir / recursive mkdir
{
const realVfs = vfs.create(new vfs.RealFSProvider(testDir));
realVfs.mkdirSync('/new-dir');
assert.strictEqual(realVfs.mkdirSync('/new-dir'), undefined);
assert.strictEqual(fs.existsSync(path.join(testDir, 'new-dir')), true);
realVfs.rmdirSync('/new-dir');
assert.strictEqual(fs.existsSync(path.join(testDir, 'new-dir')), false);

realVfs.mkdirSync('/deep/nested/dir', { recursive: true });
// Recursive mkdir returns the first created directory as a VFS path, never
// as a path inside the provider root.
const created = realVfs.mkdirSync('/deep/nested/dir', { recursive: true });
assert.strictEqual(created, '/deep');
assert.strictEqual(fs.existsSync(path.join(testDir, 'deep/nested/dir')), true);

// Nothing to create resolves to undefined.
assert.strictEqual(realVfs.mkdirSync('/deep/nested', { recursive: true }),
undefined);
// Only the first missing component is reported.
assert.strictEqual(realVfs.mkdirSync('/deep/nested/dir/x/y',
{ recursive: true }), '/deep/nested/dir/x');
fs.rmdirSync(path.join(testDir, 'deep/nested/dir/x/y'));
fs.rmdirSync(path.join(testDir, 'deep/nested/dir/x'));
fs.rmdirSync(path.join(testDir, 'deep/nested/dir'));
fs.rmdirSync(path.join(testDir, 'deep/nested'));
fs.rmdirSync(path.join(testDir, 'deep'));
Expand Down
Loading