Skip to content

Commit 1dee71c

Browse files
vfs: route mkdtempDisposable through the fs hooks
fs.mkdtempSync() and fsPromises.mkdtemp() consult the VFS handlers, but fs.mkdtempDisposableSync() and fsPromises.mkdtempDisposable() call the binding directly. On a mounted path they fail with ENOTDIR, because the mount point does not exist on the real file system, while the non-disposable variants succeed. Add the two handlers. They create the directory with the VFS mkdtemp and remove it with the VFS rm, so the disposable object behaves like the real one: the path is a Buffer when the buffer encoding is requested, and remove() can be called more than once. Refs: #65852 Signed-off-by: Samuel Mbabhazi <samuelmbabhazi@gmail.com>
1 parent cd192cd commit 1dee71c

4 files changed

Lines changed: 152 additions & 0 deletions

File tree

‎lib/fs.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3750,6 +3750,12 @@ function mkdtempSync(prefix, options) {
37503750
* @returns {object} A disposable object with a "path" property.
37513751
*/
37523752
function mkdtempDisposableSync(prefix, options) {
3753+
const h = vfsState.handlers;
3754+
if (h !== null) {
3755+
const result = h.mkdtempDisposableSync(prefix, options);
3756+
if (result !== undefined) return result;
3757+
}
3758+
37533759
options = getOptions(options);
37543760
if (BufferIsBuffer(prefix)) {
37553761
options = { ...options, encoding: 'buffer' };

‎lib/internal/fs/promises.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2159,6 +2159,12 @@ async function mkstemp(prefix, options) {
21592159
}
21602160

21612161
async function mkdtempDisposable(prefix, options) {
2162+
const h = vfsState.handlers;
2163+
if (h !== null) {
2164+
const promise = h.mkdtempDisposable(prefix, options);
2165+
if (promise !== undefined) return await promise;
2166+
}
2167+
21622168
options = getOptions(options);
21632169
if (BufferIsBuffer(prefix)) {
21642170
options = { ...options, encoding: 'buffer' };

‎lib/internal/vfs/setup.js‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const {
99
String,
1010
StringPrototypeEndsWith,
1111
StringPrototypeStartsWith,
12+
SymbolAsyncDispose,
13+
SymbolDispose,
1214
} = primordials;
1315

1416
const { Buffer } = require('buffer');
@@ -539,6 +541,19 @@ function createVfsHandlers() {
539541
}
540542
return result;
541543
},
544+
mkdtempDisposableSync(prefix, options) {
545+
return vfsOp(prefix, (vfs, n) => {
546+
const dir = vfs.mkdtempSync(n);
547+
const remove = () => vfs.rmSync(dir, { recursive: true, force: true });
548+
return {
549+
path: options?.encoding === 'buffer' ? Buffer.from(dir) : dir,
550+
remove,
551+
[SymbolDispose]() {
552+
remove();
553+
},
554+
};
555+
});
556+
},
542557
opendirSync: (path, options) => vfsOp(path, (vfs, n) => vfs.opendirSync(n, options)),
543558
openAsBlob(path, options) {
544559
const pathStr = toPathStr(path);
@@ -890,6 +905,20 @@ function createVfsHandlers() {
890905
};
891906
});
892907
},
908+
mkdtempDisposable(prefix, options) {
909+
return vfsOp(prefix, async (vfs, n) => {
910+
const dir = await vfs.promises.mkdtemp(n);
911+
const remove = () => vfs.promises.rm(dir, { recursive: true, force: true });
912+
return {
913+
__proto__: null,
914+
path: options?.encoding === 'buffer' ? Buffer.from(dir) : dir,
915+
remove,
916+
async [SymbolAsyncDispose]() {
917+
await remove();
918+
},
919+
};
920+
});
921+
},
893922
chmod: (path, mode) =>
894923
vfsOp(path, (vfs, n) => vfs.promises.chmod(n, mode).then(() => true)),
895924
utimes: (path, atime, mtime) =>
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Flags: --experimental-vfs
2+
'use strict';
3+
4+
// fs.mkdtempDisposableSync and fs.promises.mkdtempDisposable dispatch to the
5+
// VFS for a mounted prefix, like mkdtempSync and fs.promises.mkdtemp do, and
6+
// their remove() deletes the directory from the VFS. A prefix outside the
7+
// mount still reaches the real file system.
8+
9+
const common = require('../common');
10+
const assert = require('assert');
11+
const fs = require('fs');
12+
const path = require('path');
13+
const vfs = require('node:vfs');
14+
const tmpdir = require('../common/tmpdir');
15+
16+
tmpdir.refresh();
17+
18+
const myVfs = vfs.create();
19+
myVfs.mkdirSync('/work', { recursive: true });
20+
const mountPoint = myVfs.mount();
21+
const prefix = path.join(mountPoint, 'work', 'tmp-');
22+
23+
// Sync: created in the VFS, removed from the VFS, removal is idempotent.
24+
{
25+
const result = fs.mkdtempDisposableSync(prefix);
26+
assert.ok(result.path.startsWith(prefix));
27+
assert.strictEqual(result.path.length, prefix.length + 6);
28+
assert.strictEqual(fs.statSync(result.path).isDirectory(), true);
29+
assert.deepStrictEqual(fs.readdirSync(path.join(mountPoint, 'work')), [path.basename(result.path)]);
30+
31+
fs.writeFileSync(path.join(result.path, 'file.txt'), 'data');
32+
result.remove();
33+
assert.strictEqual(fs.existsSync(result.path), false);
34+
assert.deepStrictEqual(fs.readdirSync(path.join(mountPoint, 'work')), []);
35+
result.remove();
36+
}
37+
38+
// Sync: Symbol.dispose
39+
{
40+
const result = fs.mkdtempDisposableSync(prefix);
41+
assert.strictEqual(fs.existsSync(result.path), true);
42+
result[Symbol.dispose]();
43+
assert.strictEqual(fs.existsSync(result.path), false);
44+
result[Symbol.dispose]();
45+
}
46+
47+
// Sync: buffer encoding
48+
{
49+
const result = fs.mkdtempDisposableSync(prefix, { encoding: 'buffer' });
50+
assert.ok(Buffer.isBuffer(result.path));
51+
assert.ok(result.path.toString().startsWith(prefix));
52+
assert.strictEqual(fs.existsSync(result.path), true);
53+
result.remove();
54+
assert.strictEqual(fs.existsSync(result.path), false);
55+
}
56+
57+
// Sync: a prefix outside the mount uses the real file system.
58+
{
59+
const result = fs.mkdtempDisposableSync(tmpdir.resolve('real-'));
60+
assert.strictEqual(path.dirname(result.path), tmpdir.path);
61+
assert.strictEqual(fs.existsSync(result.path), true);
62+
result.remove();
63+
assert.strictEqual(fs.existsSync(result.path), false);
64+
}
65+
66+
(async () => {
67+
// Promises: created in the VFS, removed from the VFS, removal is idempotent.
68+
{
69+
const result = await fs.promises.mkdtempDisposable(prefix);
70+
assert.ok(result.path.startsWith(prefix));
71+
assert.strictEqual(result.path.length, prefix.length + 6);
72+
assert.strictEqual(fs.statSync(result.path).isDirectory(), true);
73+
assert.deepStrictEqual(fs.readdirSync(path.join(mountPoint, 'work')), [path.basename(result.path)]);
74+
75+
fs.writeFileSync(path.join(result.path, 'file.txt'), 'data');
76+
await result.remove();
77+
assert.strictEqual(fs.existsSync(result.path), false);
78+
assert.deepStrictEqual(fs.readdirSync(path.join(mountPoint, 'work')), []);
79+
await result.remove();
80+
}
81+
82+
// Promises: Symbol.asyncDispose
83+
{
84+
const result = await fs.promises.mkdtempDisposable(prefix);
85+
assert.strictEqual(fs.existsSync(result.path), true);
86+
await result[Symbol.asyncDispose]();
87+
assert.strictEqual(fs.existsSync(result.path), false);
88+
await result[Symbol.asyncDispose]();
89+
}
90+
91+
// Promises: buffer encoding
92+
{
93+
const result = await fs.promises.mkdtempDisposable(prefix, { encoding: 'buffer' });
94+
assert.ok(Buffer.isBuffer(result.path));
95+
assert.ok(result.path.toString().startsWith(prefix));
96+
assert.strictEqual(fs.existsSync(result.path), true);
97+
await result.remove();
98+
assert.strictEqual(fs.existsSync(result.path), false);
99+
}
100+
101+
// Promises: a prefix outside the mount uses the real file system.
102+
{
103+
const result = await fs.promises.mkdtempDisposable(tmpdir.resolve('real-'));
104+
assert.strictEqual(path.dirname(result.path), tmpdir.path);
105+
assert.strictEqual(fs.existsSync(result.path), true);
106+
await result.remove();
107+
assert.strictEqual(fs.existsSync(result.path), false);
108+
}
109+
110+
myVfs.unmount();
111+
})().then(common.mustCall());

0 commit comments

Comments
 (0)