Skip to content

Commit 1a885ca

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> PR-URL: #66484 Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent 06550c7 commit 1a885ca

22 files changed

Lines changed: 1095 additions & 17 deletions

‎doc/api/fs.md‎

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1732,6 +1732,58 @@ For detailed information, see the documentation of [`fsPromises.mkdtemp()`][].
17321732
The optional `options` argument can be a string specifying an encoding, or an
17331733
object with an `encoding` property specifying the character encoding to use.
17341734
1735+
### `fsPromises.mkstemp(prefix[, options])`
1736+
1737+
<!-- YAML
1738+
added: REPLACEME
1739+
-->
1740+
1741+
> Stability: 1 - Experimental
1742+
1743+
* `prefix` {string|Buffer|URL}
1744+
* `options` {string|Object}
1745+
* `encoding` {string} **Default:** `'utf8'` (or `'buffer'` if `prefix` is a `Buffer`)
1746+
* Returns: {Promise} Fulfills with an {Object}:
1747+
* `path` {string|Buffer} The path of the created file.
1748+
* `handle` {FileHandle} The created file, opened for reading and writing.
1749+
1750+
Creates and opens a unique temporary file. A unique file name is generated by
1751+
appending six random characters to the end of the provided `prefix`. Due to
1752+
platform inconsistencies, avoid trailing `X` characters in `prefix`. Some
1753+
platforms, notably the BSDs, can return more than six random characters, and
1754+
replace trailing `X` characters in `prefix` with random characters.
1755+
1756+
The file is created and opened in a single operation, so another process cannot
1757+
create a file with the same name in between. On POSIX systems, the file is only
1758+
readable and writable by its owner.
1759+
1760+
The optional `options` argument can be a string specifying an encoding, or an
1761+
object with an `encoding` property specifying the character encoding to use for
1762+
the returned `path`.
1763+
1764+
```mjs
1765+
import { mkstemp } from 'node:fs/promises';
1766+
import { join } from 'node:path';
1767+
import { tmpdir } from 'node:os';
1768+
1769+
const { path, handle } = await mkstemp(join(tmpdir(), 'foo-'));
1770+
try {
1771+
await handle.writeFile('some data');
1772+
console.log(path);
1773+
// Prints: /tmp/foo-itXde2 or C:\Users\...\AppData\Local\Temp\foo-itXde2
1774+
} finally {
1775+
await handle.close();
1776+
}
1777+
```
1778+
1779+
The file is not removed automatically. It is the caller's responsibility to
1780+
close the {FileHandle} and to remove the file when it is no longer needed.
1781+
1782+
As with [`fsPromises.mkdtemp()`][], the random characters are appended directly
1783+
to `prefix`. To create a file _within_ a directory, `prefix` must end with a
1784+
trailing platform-specific path separator (`require('node:path').sep`) or
1785+
include the beginning of the file name.
1786+
17351787
### `fsPromises.open(path, flags[, mode])`
17361788
17371789
<!-- YAML
@@ -4099,6 +4151,66 @@ mkdtemp(`${tmpDir}${sep}`, (err, directory) => {
40994151
});
41004152
```
41014153
4154+
### `fs.mkstemp(prefix[, options], callback)`
4155+
4156+
<!-- YAML
4157+
added: REPLACEME
4158+
-->
4159+
4160+
> Stability: 1 - Experimental
4161+
4162+
* `prefix` {string|Buffer|URL}
4163+
* `options` {string|Object}
4164+
* `encoding` {string} **Default:** `'utf8'` (or `'buffer'` if `prefix` is a `Buffer`)
4165+
* `callback` {Function}
4166+
* `err` {Error}
4167+
* `file` {Object}
4168+
* `path` {string|Buffer} The path of the created file.
4169+
* `fd` {integer} A file descriptor for the created file, opened for
4170+
reading and writing.
4171+
4172+
Creates and opens a unique temporary file.
4173+
4174+
Generates six random characters to be appended behind a required `prefix` to
4175+
create a unique temporary file. Due to platform inconsistencies, avoid trailing
4176+
`X` characters in `prefix`. Some platforms, notably the BSDs, can return more
4177+
than six random characters, and replace trailing `X` characters in `prefix`
4178+
with random characters.
4179+
4180+
The file is created and opened in a single operation, so another process cannot
4181+
create a file with the same name in between. On POSIX systems, the file is only
4182+
readable and writable by its owner.
4183+
4184+
The optional `options` argument can be a string specifying an encoding, or an
4185+
object with an `encoding` property specifying the character encoding to use for
4186+
the `path` passed to the callback.
4187+
4188+
```mjs
4189+
import { mkstemp, write, close } from 'node:fs';
4190+
import { join } from 'node:path';
4191+
import { tmpdir } from 'node:os';
4192+
4193+
mkstemp(join(tmpdir(), 'foo-'), (err, file) => {
4194+
if (err) throw err;
4195+
console.log(file.path);
4196+
// Prints: /tmp/foo-itXde2 or C:\Users\...\AppData\Local\Temp\foo-itXde2
4197+
write(file.fd, 'some data', (err) => {
4198+
if (err) throw err;
4199+
close(file.fd, (err) => {
4200+
if (err) throw err;
4201+
});
4202+
});
4203+
});
4204+
```
4205+
4206+
The file is not removed automatically. It is the caller's responsibility to
4207+
close the file descriptor and to remove the file when it is no longer needed.
4208+
4209+
As with [`fs.mkdtemp()`][], the random characters are appended directly to
4210+
`prefix`. To create a file _within_ a directory, `prefix` must end with a
4211+
trailing platform-specific path separator (`require('node:path').sep`) or
4212+
include the beginning of the file name.
4213+
41024214
### `fs.open(path[, flags[, mode]], callback)`
41034215
41044216
<!-- YAML
@@ -6583,6 +6695,31 @@ with the [`using`][] syntax.
65836695
The optional `options` argument can be a string specifying an encoding, or an
65846696
object with an `encoding` property specifying the character encoding to use.
65856697
6698+
### `fs.mkstempSync(prefix[, options])`
6699+
6700+
<!-- YAML
6701+
added: REPLACEME
6702+
-->
6703+
6704+
> Stability: 1 - Experimental
6705+
6706+
* `prefix` {string|Buffer|URL}
6707+
* `options` {string|Object}
6708+
* `encoding` {string} **Default:** `'utf8'` (or `'buffer'` if `prefix` is a `Buffer`)
6709+
* Returns: {Object}
6710+
* `path` {string|Buffer} The path of the created file.
6711+
* `fd` {integer} A file descriptor for the created file, opened for reading
6712+
and writing.
6713+
6714+
Synchronously creates and opens a unique temporary file.
6715+
6716+
For detailed information, see the documentation of the asynchronous version of
6717+
this API: [`fs.mkstemp()`][].
6718+
6719+
The optional `options` argument can be a string specifying an encoding, or an
6720+
object with an `encoding` property specifying the character encoding to use for
6721+
the returned `path`.
6722+
65866723
### `fs.openAsBlobSync(path[, options])`
65876724
65886725
<!-- YAML
@@ -9610,6 +9747,7 @@ the file contents.
96109747
[`fs.lutimes()`]: #fslutimespath-atime-mtime-callback
96119748
[`fs.mkdir()`]: #fsmkdirpath-options-callback
96129749
[`fs.mkdtemp()`]: #fsmkdtempprefix-options-callback
9750+
[`fs.mkstemp()`]: #fsmkstempprefix-options-callback
96139751
[`fs.open()`]: #fsopenpath-flags-mode-callback
96149752
[`fs.openAsBlob()`]: #fsopenasblobpath-options
96159753
[`fs.opendir()`]: #fsopendirpath-options-callback

‎doc/api/vfs.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ signatures as their [`node:fs`][] counterparts:
376376
* `utimesSync(path, atime, mtime)`
377377
* `lutimesSync(path, atime, mtime)`
378378
* `mkdtempSync(prefix)`
379+
* `mkstempSync(prefix)`
379380
* `opendirSync(path[, options])`
380381
* `openAsBlob(path[, options])`
381382
* File-descriptor ops: `openSync`, `closeSync`, `readSync`, `writeSync`,
@@ -387,8 +388,8 @@ signatures as their [`node:fs`][] counterparts:
387388

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

393394
#### Promise API
394395

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

415416
## The reserved root directory
416417

‎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
@@ -2136,6 +2136,28 @@ async function mkdtemp(prefix, options) {
21362136
);
21372137
}
21382138

2139+
async function mkstemp(prefix, options) {
2140+
options = getOptions(options);
2141+
if (BufferIsBuffer(prefix)) {
2142+
options = { ...options, encoding: 'buffer' };
2143+
}
2144+
prefix = getValidatedPath(prefix, 'prefix');
2145+
warnOnNonPortableTemplate(prefix);
2146+
2147+
const h = vfsState.handlers;
2148+
if (h !== null) {
2149+
const promise = h.promisesMkstemp(prefix, options);
2150+
if (promise !== undefined) return await promise;
2151+
}
2152+
2153+
const result = await PromisePrototypeThen(
2154+
binding.mkstemp(prefix, options.encoding, kUsePromises),
2155+
undefined,
2156+
handleErrorFromBinding,
2157+
);
2158+
return { __proto__: null, path: result[0], handle: new FileHandle(result[1]) };
2159+
}
2160+
21392161
async function mkdtempDisposable(prefix, options) {
21402162
options = getOptions(options);
21412163
if (BufferIsBuffer(prefix)) {
@@ -2389,6 +2411,7 @@ module.exports = {
23892411
realpath,
23902412
mkdtemp,
23912413
mkdtempDisposable,
2414+
mkstemp,
23922415
writeFile,
23932416
appendFile,
23942417
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
@@ -614,8 +614,19 @@ class VirtualFileSystem {
614614
}
615615

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

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

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

0 commit comments

Comments
 (0)