Skip to content

Commit c7570b3

Browse files
fs: add mkstemp()
Add fs.mkstemp(), fs.mkstempSync() and fsPromises.mkstemp(), which create and open a unique temporary file in one operation. They expose uv_fs_mkstemp(), the file counterpart of the function behind fs.mkdtemp(). The callback and sync versions return the path and a file descriptor, the promise version returns the path and a FileHandle. libuv clears the path of the request when mkstemp() fails, so the template is saved in the request and used for the path of the error. The functions are also supported on mounted virtual file systems. Refs: #33549 Refs: #33890 Refs: #5332 Signed-off-by: marcopiraccini <marco.piraccini@gmail.com>
1 parent c6d52f5 commit c7570b3

19 files changed

Lines changed: 742 additions & 15 deletions

‎doc/api/fs.md‎

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1726,6 +1726,56 @@ For detailed information, see the documentation of [`fsPromises.mkdtemp()`][].
17261726
The optional `options` argument can be a string specifying an encoding, or an
17271727
object with an `encoding` property specifying the character encoding to use.
17281728
1729+
### `fsPromises.mkstemp(prefix[, options])`
1730+
1731+
<!-- YAML
1732+
added: REPLACEME
1733+
-->
1734+
1735+
* `prefix` {string|Buffer|URL}
1736+
* `options` {string|Object}
1737+
* `encoding` {string} **Default:** `'utf8'` (or `'buffer'` if `prefix` is a `Buffer`)
1738+
* Returns: {Promise} Fulfills with an {Object}:
1739+
* `path` {string|Buffer} The path of the created file.
1740+
* `handle` {FileHandle} The created file, opened for reading and writing.
1741+
1742+
Creates and opens a unique temporary file. A unique file name is generated by
1743+
appending six random characters to the end of the provided `prefix`. Due to
1744+
platform inconsistencies, avoid trailing `X` characters in `prefix`. Some
1745+
platforms, notably the BSDs, can return more than six random characters, and
1746+
replace trailing `X` characters in `prefix` with random characters.
1747+
1748+
The file is created and opened in a single operation, so another process cannot
1749+
create a file with the same name in between. On POSIX systems, the file is only
1750+
readable and writable by its owner.
1751+
1752+
The optional `options` argument can be a string specifying an encoding, or an
1753+
object with an `encoding` property specifying the character encoding to use for
1754+
the returned `path`.
1755+
1756+
```mjs
1757+
import { mkstemp } from 'node:fs/promises';
1758+
import { join } from 'node:path';
1759+
import { tmpdir } from 'node:os';
1760+
1761+
const { path, handle } = await mkstemp(join(tmpdir(), 'foo-'));
1762+
try {
1763+
await handle.writeFile('some data');
1764+
console.log(path);
1765+
// Prints: /tmp/foo-itXde2 or C:\Users\...\AppData\Local\Temp\foo-itXde2
1766+
} finally {
1767+
await handle.close();
1768+
}
1769+
```
1770+
1771+
The file is not removed automatically. It is the caller's responsibility to
1772+
close the {FileHandle} and to remove the file when it is no longer needed.
1773+
1774+
As with [`fsPromises.mkdtemp()`][], the random characters are appended directly
1775+
to `prefix`. To create a file _within_ a directory, `prefix` must end with a
1776+
trailing platform-specific path separator (`require('node:path').sep`) or
1777+
include the beginning of the file name.
1778+
17291779
### `fsPromises.open(path, flags[, mode])`
17301780
17311781
<!-- YAML
@@ -4093,6 +4143,64 @@ mkdtemp(`${tmpDir}${sep}`, (err, directory) => {
40934143
});
40944144
```
40954145
4146+
### `fs.mkstemp(prefix[, options], callback)`
4147+
4148+
<!-- YAML
4149+
added: REPLACEME
4150+
-->
4151+
4152+
* `prefix` {string|Buffer|URL}
4153+
* `options` {string|Object}
4154+
* `encoding` {string} **Default:** `'utf8'` (or `'buffer'` if `prefix` is a `Buffer`)
4155+
* `callback` {Function}
4156+
* `err` {Error}
4157+
* `file` {Object}
4158+
* `path` {string|Buffer} The path of the created file.
4159+
* `fd` {integer} A file descriptor for the created file, opened for
4160+
reading and writing.
4161+
4162+
Creates and opens a unique temporary file.
4163+
4164+
Generates six random characters to be appended behind a required `prefix` to
4165+
create a unique temporary file. Due to platform inconsistencies, avoid trailing
4166+
`X` characters in `prefix`. Some platforms, notably the BSDs, can return more
4167+
than six random characters, and replace trailing `X` characters in `prefix`
4168+
with random characters.
4169+
4170+
The file is created and opened in a single operation, so another process cannot
4171+
create a file with the same name in between. On POSIX systems, the file is only
4172+
readable and writable by its owner.
4173+
4174+
The optional `options` argument can be a string specifying an encoding, or an
4175+
object with an `encoding` property specifying the character encoding to use for
4176+
the `path` passed to the callback.
4177+
4178+
```mjs
4179+
import { mkstemp, write, close } from 'node:fs';
4180+
import { join } from 'node:path';
4181+
import { tmpdir } from 'node:os';
4182+
4183+
mkstemp(join(tmpdir(), 'foo-'), (err, file) => {
4184+
if (err) throw err;
4185+
console.log(file.path);
4186+
// Prints: /tmp/foo-itXde2 or C:\Users\...\AppData\Local\Temp\foo-itXde2
4187+
write(file.fd, 'some data', (err) => {
4188+
if (err) throw err;
4189+
close(file.fd, (err) => {
4190+
if (err) throw err;
4191+
});
4192+
});
4193+
});
4194+
```
4195+
4196+
The file is not removed automatically. It is the caller's responsibility to
4197+
close the file descriptor and to remove the file when it is no longer needed.
4198+
4199+
As with [`fs.mkdtemp()`][], the random characters are appended directly to
4200+
`prefix`. To create a file _within_ a directory, `prefix` must end with a
4201+
trailing platform-specific path separator (`require('node:path').sep`) or
4202+
include the beginning of the file name.
4203+
40964204
### `fs.open(path[, flags[, mode]], callback)`
40974205
40984206
<!-- YAML
@@ -6577,6 +6685,29 @@ with the [`using`][] syntax.
65776685
The optional `options` argument can be a string specifying an encoding, or an
65786686
object with an `encoding` property specifying the character encoding to use.
65796687
6688+
### `fs.mkstempSync(prefix[, options])`
6689+
6690+
<!-- YAML
6691+
added: REPLACEME
6692+
-->
6693+
6694+
* `prefix` {string|Buffer|URL}
6695+
* `options` {string|Object}
6696+
* `encoding` {string} **Default:** `'utf8'` (or `'buffer'` if `prefix` is a `Buffer`)
6697+
* Returns: {Object}
6698+
* `path` {string|Buffer} The path of the created file.
6699+
* `fd` {integer} A file descriptor for the created file, opened for reading
6700+
and writing.
6701+
6702+
Synchronously creates and opens a unique temporary file.
6703+
6704+
For detailed information, see the documentation of the asynchronous version of
6705+
this API: [`fs.mkstemp()`][].
6706+
6707+
The optional `options` argument can be a string specifying an encoding, or an
6708+
object with an `encoding` property specifying the character encoding to use for
6709+
the returned `path`.
6710+
65806711
### `fs.openAsBlobSync(path[, options])`
65816712
65826713
<!-- YAML
@@ -9597,6 +9728,7 @@ the file contents.
95979728
[`fs.lutimes()`]: #fslutimespath-atime-mtime-callback
95989729
[`fs.mkdir()`]: #fsmkdirpath-options-callback
95999730
[`fs.mkdtemp()`]: #fsmkdtempprefix-options-callback
9731+
[`fs.mkstemp()`]: #fsmkstempprefix-options-callback
96009732
[`fs.open()`]: #fsopenpath-flags-mode-callback
96019733
[`fs.openAsBlob()`]: #fsopenasblobpath-options
96029734
[`fs.opendir()`]: #fsopendirpath-options-callback

‎doc/api/vfs.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ signatures as their [`node:fs`][] counterparts:
374374
* `utimesSync(path, atime, mtime)`
375375
* `lutimesSync(path, atime, mtime)`
376376
* `mkdtempSync(prefix)`
377+
* `mkstempSync(prefix)`
377378
* `opendirSync(path[, options])`
378379
* `openAsBlob(path[, options])`
379380
* File-descriptor ops: `openSync`, `closeSync`, `readSync`, `writeSync`,
@@ -385,8 +386,8 @@ signatures as their [`node:fs`][] counterparts:
385386

386387
`readFile`, `writeFile`, `stat`, `lstat`, `readdir`, `realpath`, `readlink`,
387388
`access`, `open`, `close`, `read`, `write`, `rm`, `fstat`, `truncate`,
388-
`ftruncate`, `link`, `mkdtemp`, `opendir`. Each takes a Node.js-style
389-
callback `(err, ...result) => {}`.
389+
`ftruncate`, `link`, `mkdtemp`, `mkstemp`, `opendir`. Each takes a
390+
Node.js-style callback `(err, ...result) => {}`.
390391

391392
#### Promise API
392393

@@ -407,8 +408,8 @@ example();
407408
The promise namespace mirrors `fs.promises` and includes `readFile`,
408409
`writeFile`, `appendFile`, `stat`, `lstat`, `readdir`, `mkdir`, `rmdir`,
409410
`unlink`, `rename`, `copyFile`, `realpath`, `readlink`, `symlink`,
410-
`access`, `rm`, `truncate`, `link`, `mkdtemp`, `chmod`, `chown`, `lchown`,
411-
`utimes`, `lutimes`, `open`, `lchmod`, and `watch`.
411+
`access`, `rm`, `truncate`, `link`, `mkdtemp`, `mkstemp`, `chmod`, `chown`,
412+
`lchown`, `utimes`, `lutimes`, `open`, `lchmod`, and `watch`.
412413

413414
## The reserved root directory
414415

‎lib/fs.js‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3773,6 +3773,58 @@ function mkdtempDisposableSync(prefix, options) {
37733773
};
37743774
}
37753775

3776+
/**
3777+
* Creates and opens a unique temporary file.
3778+
* @param {string | Buffer | URL} prefix
3779+
* @param {string | { encoding?: string; }} [options]
3780+
* @param {(err?: Error, file?: { path: string | Buffer, fd: number }) => any} callback
3781+
* @returns {void}
3782+
*/
3783+
function mkstemp(prefix, options, callback) {
3784+
callback = makeCallback(typeof options === 'function' ? options : callback);
3785+
3786+
options = getOptions(options);
3787+
if (BufferIsBuffer(prefix)) {
3788+
options = { ...options, encoding: 'buffer' };
3789+
}
3790+
prefix = getValidatedPath(prefix, 'prefix');
3791+
warnOnNonPortableTemplate(prefix);
3792+
3793+
const h = vfsState.handlers;
3794+
if (h !== null && vfsResult(h.mkstemp(prefix, options), callback)) return;
3795+
3796+
const req = new FSReqCallback();
3797+
req.oncomplete = (err, result) => {
3798+
if (err) return callback(err);
3799+
callback(null, { path: result[0], fd: result[1] });
3800+
};
3801+
binding.mkstemp(prefix, options.encoding, req);
3802+
}
3803+
3804+
/**
3805+
* Synchronously creates and opens a unique temporary file.
3806+
* @param {string | Buffer | URL} prefix
3807+
* @param {string | { encoding?: string; }} [options]
3808+
* @returns {{ path: string | Buffer, fd: number }}
3809+
*/
3810+
function mkstempSync(prefix, options) {
3811+
options = getOptions(options);
3812+
if (BufferIsBuffer(prefix)) {
3813+
options = { ...options, encoding: 'buffer' };
3814+
}
3815+
prefix = getValidatedPath(prefix, 'prefix');
3816+
warnOnNonPortableTemplate(prefix);
3817+
3818+
const h = vfsState.handlers;
3819+
if (h !== null) {
3820+
const result = h.mkstempSync(prefix, options);
3821+
if (result !== undefined) return result;
3822+
}
3823+
3824+
const result = binding.mkstemp(prefix, options.encoding);
3825+
return { path: result[0], fd: result[1] };
3826+
}
3827+
37763828
/**
37773829
* Asynchronously copies `src` to `dest`. By
37783830
* default, `dest` is overwritten if it already exists.
@@ -3997,6 +4049,8 @@ module.exports = fs = {
39974049
mkdtemp,
39984050
mkdtempSync,
39994051
mkdtempDisposableSync,
4052+
mkstemp,
4053+
mkstempSync,
40004054
open,
40014055
openSync,
40024056
openAsBlob,

‎lib/internal/fs/promises.js‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2094,6 +2094,28 @@ async function mkdtemp(prefix, options) {
20942094
);
20952095
}
20962096

2097+
async function mkstemp(prefix, options) {
2098+
options = getOptions(options);
2099+
if (BufferIsBuffer(prefix)) {
2100+
options = { ...options, encoding: 'buffer' };
2101+
}
2102+
prefix = getValidatedPath(prefix, 'prefix');
2103+
warnOnNonPortableTemplate(prefix);
2104+
2105+
const h = vfsState.handlers;
2106+
if (h !== null) {
2107+
const promise = h.promisesMkstemp(prefix, options);
2108+
if (promise !== undefined) return await promise;
2109+
}
2110+
2111+
const result = await PromisePrototypeThen(
2112+
binding.mkstempFileHandle(prefix, options.encoding, kUsePromises),
2113+
undefined,
2114+
handleErrorFromBinding,
2115+
);
2116+
return { __proto__: null, path: result[0], handle: new FileHandle(result[1]) };
2117+
}
2118+
20972119
async function mkdtempDisposable(prefix, options) {
20982120
options = getOptions(options);
20992121
if (BufferIsBuffer(prefix)) {
@@ -2347,6 +2369,7 @@ module.exports = {
23472369
realpath,
23482370
mkdtemp,
23492371
mkdtempDisposable,
2372+
mkstemp,
23502373
writeFile,
23512374
appendFile,
23522375
readFile,

‎lib/internal/fs/utils.js‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -965,12 +965,14 @@ const validateBufferArray = hideStackFrames((buffers, propName = 'buffers') => {
965965
let nonPortableTemplateWarn = true;
966966

967967
function warnOnNonPortableTemplate(template) {
968-
// Template strings passed to the mkdtemp() family of functions should not
969-
// end with 'X' because they are handled inconsistently across platforms.
968+
// Template strings passed to the mkdtemp() and mkstemp() families of
969+
// functions should not end with 'X' because they are handled inconsistently
970+
// across platforms.
970971
if (nonPortableTemplateWarn &&
971972
((typeof template === 'string' && StringPrototypeEndsWith(template, 'X')) ||
972973
(typeof template !== 'string' && TypedArrayPrototypeAt(template, -1) === 0x58))) {
973-
process.emitWarning('mkdtemp() templates ending with X are not portable. ' +
974+
process.emitWarning('mkdtemp() and mkstemp() templates ending with X are ' +
975+
'not portable. ' +
974976
'For details see: https://nodejs.org/api/fs.html');
975977
nonPortableTemplateWarn = false;
976978
}

‎lib/internal/vfs/file_system.js‎

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -615,8 +615,19 @@ class VirtualFileSystem {
615615
}
616616

617617
/**
618-
* Converts a mkdtemp prefix to a provider-relative one, keeping a
619-
* trailing separator.
618+
* Creates and opens a unique temporary file synchronously.
619+
* @param {string} prefix The prefix for the temp file
620+
* @returns {{ path: string, fd: number }} The full path and a file descriptor
621+
*/
622+
mkstempSync(prefix) {
623+
const filePath = this.#toProviderPrefix(prefix) + randomSuffix();
624+
const handle = this[kProvider].openSync(filePath, 'wx+', 0o600);
625+
return { path: this.#toMountedPath(filePath), fd: openVirtualFd(handle) };
626+
}
627+
628+
/**
629+
* Converts a mkdtemp or mkstemp prefix to a provider-relative one, keeping
630+
* a trailing separator.
620631
* @param {string} prefix The mounted prefix
621632
* @returns {string}
622633
*/
@@ -1058,6 +1069,25 @@ class VirtualFileSystem {
10581069
}
10591070
}
10601071

1072+
/**
1073+
* Creates and opens a unique temporary file asynchronously.
1074+
* @param {string} prefix The prefix for the temp file
1075+
* @param {object|Function} [options] Options or callback
1076+
* @param {Function} [callback] Callback (err, { path, fd })
1077+
*/
1078+
mkstemp(prefix, options, callback) {
1079+
if (typeof options === 'function') {
1080+
callback = options;
1081+
options = undefined;
1082+
}
1083+
try {
1084+
const file = this.mkstempSync(prefix);
1085+
process.nextTick(callback, null, file);
1086+
} catch (err) {
1087+
process.nextTick(callback, err);
1088+
}
1089+
}
1090+
10611091
/**
10621092
* Opens a directory asynchronously.
10631093
* @param {string} dirPath The directory path
@@ -1309,6 +1339,16 @@ class VirtualFileSystem {
13091339
return toMountedPath(dirPath);
13101340
},
13111341

1342+
async mkstemp(prefix) {
1343+
const filePath = toProviderPrefix(prefix) + randomSuffix();
1344+
const handle = provider.openSync(filePath, 'wx+', 0o600);
1345+
return {
1346+
__proto__: null,
1347+
path: toMountedPath(filePath),
1348+
fd: openVirtualFd(handle),
1349+
};
1350+
},
1351+
13121352
async chmod(filePath, mode) {
13131353
const providerPath = toProviderPath(filePath);
13141354
provider.chmodSync(providerPath, mode);

0 commit comments

Comments
 (0)