Skip to content
Closed
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
103 changes: 103 additions & 0 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -2677,6 +2677,70 @@ this API: [`fs.mkdtemp()`][].
The optional `options` argument can be a string specifying an encoding, or an
object with an `encoding` property specifying the character encoding to use.

## `fs.mkstemp(prefix[, options], callback)`
<!-- YAML
added: REPLACEME
-->

* `prefix` {string}
* `options` {string|Object}
* `encoding` {string} **Default:** `'utf8'`
* `callback` {Function}
* `err` {Error}
* `file` {Object}
* `path` {string}
* `fd` {number}

Creates and opens a unique temporary file.

Generates six random characters to be appended behind a required `prefix` to
create a unique temporary file. Due to platform inconsistencies, avoid trailing
`X` characters in `prefix`. Some platforms, notably the BSDs, can return more
than six random characters, and replace trailing `X` characters in `prefix`
with random characters.

The created file path and file descriptor are passed as an object to the
callback's second parameter.

The optional `options` argument can be a string specifying an encoding, or an
object with an `encoding` property specifying the character encoding to use.

```js
fs.mkstemp(path.join(os.tmpdir(), 'foo-'), (err, result) => {
if (err) throw err;
console.log(result.path);
// Prints: /tmp/foo-itXde2 or C:\Users\...\AppData\Local\Temp\foo-itXde2
fs.close(result.fd, (err) => {
if (err) throw err;
});
});
```

The `fs.mkstemp()` method will append the six randomly selected characters
directly to the `prefix` string. For instance, given a directory `/tmp`, if the
intention is to create a temporary file *within* `/tmp`, the `prefix` must end
with a trailing platform-specific path separator (`require('path').sep`).

## `fs.mkstempSync(prefix[, options])`
<!-- YAML
added: REPLACEME
-->

* `prefix` {string}
* `options` {string|Object}
* `encoding` {string} **Default:** `'utf8'`
* Returns: {Object}
* `path` {string}
* `fd` {number}

Returns an object with the created file path and file descriptor.

For detailed information, see the documentation of the asynchronous version of
this API: [`fs.mkstemp()`][].

The optional `options` argument can be a string specifying an encoding, or an
object with an `encoding` property specifying the character encoding to use.

## `fs.open(path[, flags[, mode]], callback)`
<!-- YAML
added: v0.0.2
Expand Down Expand Up @@ -5089,6 +5153,44 @@ characters directly to the `prefix` string. For instance, given a directory
`prefix` must end with a trailing platform-specific path separator
(`require('path').sep`).

### `fsPromises.mkstemp(prefix[, options])`
<!-- YAML
added: REPLACEME
-->

* `prefix` {string}
* `options` {string|Object}
* `encoding` {string} **Default:** `'utf8'`
* Returns: {Promise}

Creates and opens a unique empty temporary file, and fulfills the `Promise` with
an object that contains the created file path and a `FileHandle` object. A
unique file name is generated by appending six random characters to the end of
the provided `prefix`. Due to platform inconsistencies, avoid trailing `X`
characters in `prefix`. Some platforms, notably the BSDs, can return more than
six random characters, and replace trailing `X` characters in `prefix` with
random characters.

The optional `options` argument can be a string specifying an encoding, or an
object with an `encoding` property specifying the character encoding to use.

```js
async function createRandomFile() {
const result = await fsPromises.mkstemp(path.join(os.tmpdir(), 'foo-'));
console.log(result.path);
// Prints: /tmp/foo-itXde2 or C:\Users\...\AppData\Local\Temp\foo-itXde2
await result.handle.close();
// Closes the file descriptor but does not remove the file.
}
createRandomFile().catch(console.error);
```

The `fsPromises.mkstemp()` method will append the six randomly selected
characters directly to the `prefix` string. For instance, given a directory
`/tmp`, if the intention is to create a temporary file *within* `/tmp`, the
`prefix` must end with a trailing platform-specific path separator
(`require('path').sep`).

### `fsPromises.open(path, flags[, mode])`
<!-- YAML
added: v10.0.0
Expand Down Expand Up @@ -5828,6 +5930,7 @@ the file contents.
[`fs.lstat()`]: #fs_fs_lstat_path_options_callback
[`fs.mkdir()`]: #fs_fs_mkdir_path_options_callback
[`fs.mkdtemp()`]: #fs_fs_mkdtemp_prefix_options_callback
[`fs.mkstemp()`]: #fs_fs_mkstemp_prefix_options_callback
[`fs.open()`]: #fs_fs_open_path_flags_mode_callback
[`fs.opendir()`]: #fs_fs_opendir_path_options_callback
[`fs.opendirSync()`]: #fs_fs_opendirsync_path_options
Expand Down
42 changes: 42 additions & 0 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -1879,6 +1879,46 @@ function mkdtempSync(prefix, options) {
}


function mkstemp(prefix, options, callback) {
callback = makeCallback(typeof options === 'function' ? options : callback);
options = getOptions(options, {});
if (!prefix || typeof prefix !== 'string') {
throw new ERR_INVALID_ARG_TYPE('prefix', 'string', prefix);
}
nullCheck(prefix, 'prefix');
warnOnNonPortableTemplate(prefix);
const req = new FSReqCallback();
req.oncomplete = function oncomplete(err, result) {
if (err !== null)
return callback(err);
callback(null, {
path: result[0],
fd: result[1],
});
};
binding.mkstemp(`${prefix}XXXXXX`, options.encoding, req);
}


function mkstempSync(prefix, options) {
options = getOptions(options, {});
if (!prefix || typeof prefix !== 'string') {
throw new ERR_INVALID_ARG_TYPE('prefix', 'string', prefix);
}
nullCheck(prefix, 'prefix');
warnOnNonPortableTemplate(prefix);
const path = `${prefix}XXXXXX`;
const ctx = { path };
const result = binding.mkstemp(path, options.encoding,
undefined, ctx);
handleErrorFromBinding(ctx);
return {
path: result[0],
fd: result[1],
};
}


function copyFile(src, dest, mode, callback) {
if (typeof mode === 'function') {
callback = mode;
Expand Down Expand Up @@ -1972,6 +2012,8 @@ module.exports = fs = {
mkdirSync,
mkdtemp,
mkdtempSync,
mkstemp,
mkstempSync,
open,
openSync,
opendir,
Expand Down
19 changes: 19 additions & 0 deletions lib/internal/fs/promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,24 @@ async function mkdtemp(prefix, options) {
return binding.mkdtemp(`${prefix}XXXXXX`, options.encoding, kUsePromises);
}

async function mkstemp(prefix, options) {
options = getOptions(options, {});
if (!prefix || typeof prefix !== 'string') {
throw new ERR_INVALID_ARG_TYPE('prefix', 'string', prefix);
}
nullCheck(prefix);
warnOnNonPortableTemplate(prefix);
const result = await binding.mkstempFileHandle(
`${prefix}XXXXXX`,
options.encoding,
kUsePromises,
);
return {
path: result[0],
handle: new FileHandle(result[1]),
};
}

async function writeFile(path, data, options) {
options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'w' });
const flag = options.flag || 'w';
Expand Down Expand Up @@ -560,6 +578,7 @@ module.exports = {
utimes,
realpath,
mkdtemp,
mkstemp,
writeFile,
appendFile,
readFile,
Expand Down
3 changes: 2 additions & 1 deletion lib/internal/fs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,8 @@ function warnOnNonPortableTemplate(template) {
// Template strings passed to the mkdtemp() family of functions should not
// end with 'X' because they are handled inconsistently across platforms.
if (nonPortableTemplateWarn && template.endsWith('X')) {
process.emitWarning('mkdtemp() templates ending with X are not portable. ' +
process.emitWarning('mkdtemp() and mkstemp() templates ending with X are ' +
'not portable. ' +
'For details see: https://nodejs.org/api/fs.html');
nonPortableTemplateWarn = false;
}
Expand Down
125 changes: 125 additions & 0 deletions src/node_file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,59 @@ void AfterOpenFileHandle(uv_fs_t* req) {
}
}

void AfterMkstemp(uv_fs_t* req) {
FSReqBase* req_wrap = FSReqBase::from_req(req);
FSReqAfterScope after(req_wrap, req);

if (after.Proceed()) {
Environment* env = req_wrap->env();

Local<Value> error;
MaybeLocal<Value> path = StringBytes::Encode(
env->isolate(), req->path, req_wrap->encoding(), &error);

if (path.IsEmpty()) {
req_wrap->Reject(error);
return;
}

Local<Value> result[] = {
path.ToLocalChecked(),
Integer::New(env->isolate(), req->result)
};

req_wrap->Resolve(Array::New(env->isolate(), result, arraysize(result)));
}
}

void AfterMkstempFileHandle(uv_fs_t* req) {
FSReqBase* req_wrap = FSReqBase::from_req(req);
FSReqAfterScope after(req_wrap, req);

if (after.Proceed()) {
Environment* env = req_wrap->env();

Local<Value> error;
MaybeLocal<Value> path = StringBytes::Encode(
env->isolate(), req->path, req_wrap->encoding(), &error);

if (path.IsEmpty()) {
req_wrap->Reject(error);
return;
}

FileHandle* fd = FileHandle::New(req_wrap->binding_data(), req->result);
if (fd == nullptr) return;

Local<Value> result[] = {
path.ToLocalChecked(),
fd->object()
};

req_wrap->Resolve(Array::New(env->isolate(), result, arraysize(result)));
}
}

// Reverse the logic applied by path.toNamespacedPath() to create a
// namespace-prefixed path.
void FromNamespacedPath(std::string* path) {
Expand Down Expand Up @@ -2307,6 +2360,76 @@ static void Mkdtemp(const FunctionCallbackInfo<Value>& args) {
}
}

static void Mkstemp(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();

const int argc = args.Length();
CHECK_GE(argc, 2);

BufferValue tmpl(isolate, args[0]);
CHECK_NOT_NULL(*tmpl);

const enum encoding encoding = ParseEncoding(isolate, args[1], UTF8);

FSReqBase* req_wrap_async = GetReqWrap(args, 2);
if (req_wrap_async != nullptr) { // mkstemp(tmpl, encoding, req)
AsyncCall(env,
req_wrap_async,
args,
"mkstemp",
encoding,
AfterMkstemp,
uv_fs_mkstemp,
*tmpl);
} else { // mkstemp(tmpl, encoding, undefined, ctx)
CHECK_EQ(argc, 4);
FSReqWrapSync req_wrap_sync;
FS_SYNC_TRACE_BEGIN(mkstemp);
SyncCall(env, args[3], &req_wrap_sync, "mkstemp", uv_fs_mkstemp, *tmpl);
FS_SYNC_TRACE_END(mkstemp);
const char* path = req_wrap_sync.req.path;
int fd = req_wrap_sync.req.result;

Local<Value> error;
MaybeLocal<Value> rc = StringBytes::Encode(isolate, path, encoding, &error);
if (rc.IsEmpty()) {
Local<Object> ctx = args[3].As<Object>();
ctx->Set(env->context(), env->error_string(), error).Check();
return;
}

Local<Value> result[] = {rc.ToLocalChecked(),
Integer::New(env->isolate(), fd)};

args.GetReturnValue().Set(
Array::New(env->isolate(), result, arraysize(result)));
}
}

static void MkstempFileHandle(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();

const int argc = args.Length();
CHECK_GE(argc, 2);

BufferValue tmpl(isolate, args[0]);
CHECK_NOT_NULL(*tmpl);

const enum encoding encoding = ParseEncoding(isolate, args[1], UTF8);

FSReqBase* req_wrap_async = GetReqWrap(args, 2);
AsyncCall(env,
req_wrap_async,
args,
"mkstemp",
encoding,
AfterMkstempFileHandle,
uv_fs_mkstemp,
*tmpl);
}

void BindingData::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("stats_field_array", stats_field_array);
tracker->TrackField("stats_field_bigint_array", stats_field_bigint_array);
Expand Down Expand Up @@ -2367,6 +2490,8 @@ void Initialize(Local<Object> target,
env->SetMethod(target, "futimes", FUTimes);

env->SetMethod(target, "mkdtemp", Mkdtemp);
env->SetMethod(target, "mkstemp", Mkstemp);
env->SetMethod(target, "mkstempFileHandle", MkstempFileHandle);

target
->Set(context,
Expand Down
8 changes: 8 additions & 0 deletions test/parallel/test-fs-assert-encoding-error.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ assert.throws(() => {
fs.mkdtempSync('path', options);
}, expectedError);

assert.throws(() => {
fs.mkstemp('path', options, common.mustNotCall());
}, expectedError);

assert.throws(() => {
fs.mkstempSync('path', options);
}, expectedError);

assert.throws(() => {
fs.ReadStream('path', options);
}, expectedError);
Expand Down
Loading