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
66 changes: 48 additions & 18 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -1743,9 +1743,11 @@ added: v27.0.0-alpha.0.0.0
* `prefix` {string|Buffer|URL}
* `options` {string|Object}
* `encoding` {string} **Default:** `'utf8'` (or `'buffer'` if `prefix` is a `Buffer`)
* Returns: {Promise} Fulfills with an {Object}:
* Returns: {Promise} Fulfills with an async-disposable {Object}:
* `path` {string|Buffer} The path of the created file.
* `handle` {FileHandle} The created file, opened for reading and writing.
* `remove` {AsyncFunction} A function which closes the handle and removes the created file.
* `[Symbol.asyncDispose]` {AsyncFunction} The same as `remove`.

Creates and opens a unique temporary file. A unique file name is generated by
appending six random characters to the end of the provided `prefix`. Due to
Expand All @@ -1757,6 +1759,18 @@ The file is created and opened in a single operation, so another process cannot
create a file with the same name in between. On POSIX systems, the file is only
readable and writable by its owner.

When the returned object is disposed, its file handle will be closed and the
file will be removed asynchronously if it still exists. If the file cannot be
deleted, disposal will throw an error. The object has an async `remove()` method
which will perform the same task.

Both this function and the disposal function on the resulting object are async,
so it can be used with `await` + [`await using`][] as in
`await using file = await fsPromises.mkstemp('prefix')`.

See the [MDN documentation on `using` statements][`using`] for more information about
explicit resource management.

The optional `options` argument can be a string specifying an encoding, or an
object with an `encoding` property specifying the character encoding to use for
the returned `path`.
Expand All @@ -1766,18 +1780,16 @@ import { mkstemp } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';

const { path, handle } = await mkstemp(join(tmpdir(), 'foo-'));
try {
await handle.writeFile('some data');
console.log(path);
// Prints: /tmp/foo-itXde2 or C:\Users\...\AppData\Local\Temp\foo-itXde2
} finally {
await handle.close();
}
await using file = await mkstemp(join(tmpdir(), 'foo-'));
await file.handle.writeFile('some data');
console.log(file.path);
// Prints: /tmp/foo-itXde2 or C:\Users\...\AppData\Local\Temp\foo-itXde2
// When the scope exits, the file handle is closed and the file is deleted.
```

The file is not removed automatically. It is the caller's responsibility to
close the {FileHandle} and to remove the file when it is no longer needed.
When not using `await using`, the file can be removed manually by calling
`await file.remove()`, or the handle can be closed manually with
`await file.handle.close()`.

As with [`fsPromises.mkdtemp()`][], the random characters are appended directly
to `prefix`. To create a file _within_ a directory, `prefix` must end with a
Expand Down Expand Up @@ -4164,10 +4176,12 @@ added: v27.0.0-alpha.0.0.0
* `encoding` {string} **Default:** `'utf8'` (or `'buffer'` if `prefix` is a `Buffer`)
* `callback` {Function}
* `err` {Error}
* `file` {Object}
* `file` {Object} A disposable object:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't generally make sense for callbacks to take disposable arguments (since you can't declare a parameter with using) although I guess it doesn't necessarily hurt anything.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — since callback parameters can't be declared with using, Symbol.dispose would only run if explicitly rebound (using _ = file) or if .remove() is called directly.

Kept it here mainly for object shape parity with mkstempSync and so callers can use file.remove(), but happy to drop [Symbol.dispose] from the callback result if you prefer keeping it strictly to the sync and promises APIs.

* `path` {string|Buffer} The path of the created file.
* `fd` {integer} A file descriptor for the created file, opened for
reading and writing.
* `remove` {Function} A function which closes the file descriptor and removes the created file.
* `[Symbol.dispose]` {Function} The same as `remove`.

Creates and opens a unique temporary file.

Expand All @@ -4181,6 +4195,11 @@ The file is created and opened in a single operation, so another process cannot
create a file with the same name in between. On POSIX systems, the file is only
readable and writable by its owner.

When the `file` object is disposed, its file descriptor will be closed and the
file will be removed if it still exists. If the file cannot be deleted, disposal
will throw an error. The object has a `remove()` method which will perform the
same task.

The optional `options` argument can be a string specifying an encoding, or an
object with an `encoding` property specifying the character encoding to use for
the `path` passed to the callback.
Expand All @@ -4196,15 +4215,13 @@ mkstemp(join(tmpdir(), 'foo-'), (err, file) => {
// Prints: /tmp/foo-itXde2 or C:\Users\...\AppData\Local\Temp\foo-itXde2
write(file.fd, 'some data', (err) => {
if (err) throw err;
close(file.fd, (err) => {
if (err) throw err;
});
file.remove();
});
});
```

The file is not removed automatically. It is the caller's responsibility to
close the file descriptor and to remove the file when it is no longer needed.
When not using `file.remove()` or `file[Symbol.dispose]()`, the file descriptor
can be closed manually with `close()` or `closeSync()`.

As with [`fs.mkdtemp()`][], the random characters are appended directly to
`prefix`. To create a file _within_ a directory, `prefix` must end with a
Expand Down Expand Up @@ -6706,13 +6723,26 @@ added: v27.0.0-alpha.0.0.0
* `prefix` {string|Buffer|URL}
* `options` {string|Object}
* `encoding` {string} **Default:** `'utf8'` (or `'buffer'` if `prefix` is a `Buffer`)
* Returns: {Object}
* Returns: {Object} A disposable object:
* `path` {string|Buffer} The path of the created file.
* `fd` {integer} A file descriptor for the created file, opened for reading
and writing.
* `remove` {Function} A function which closes the file descriptor and removes the created file.
* `[Symbol.dispose]` {Function} The same as `remove`.

Synchronously creates and opens a unique temporary file.

When the returned object is disposed, its file descriptor will be closed and the
file will be removed if it still exists. If the file cannot be deleted, disposal
will throw an error. The object has a `remove()` method which will perform the
same task.

This enables the returned object to be used with the [`using`][] syntax as in
`using file = fs.mkstempSync('prefix')`.

See the [MDN documentation on `using` statements][`using`] for more information about
explicit resource management.

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

Expand Down
71 changes: 67 additions & 4 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -3835,9 +3835,16 @@ function mkdtempDisposableSync(prefix, options) {

/**
* Creates and opens a unique temporary file.
* The returned value is an object which removes the file and closes its file
* descriptor when disposed.
* @param {string | Buffer | URL} prefix
* @param {string | { encoding?: string; }} [options]
* @param {(err?: Error, file?: { path: string | Buffer, fd: number }) => any} callback
* @param {(err?: Error, file?: {
* path: string | Buffer,
* fd: number,
* remove: () => void,
* [Symbol.dispose]: () => void,
* }) => any} callback
* @returns {void}
*/
function mkstemp(prefix, options, callback) {
Expand All @@ -3853,19 +3860,51 @@ function mkstemp(prefix, options, callback) {
const h = vfsState.handlers;
if (h !== null && vfsResult(h.mkstemp(prefix, options), callback)) return;

const cwd = process.cwd();
const req = new FSReqCallback();
req.oncomplete = (err, result) => {
if (err) return callback(err);
callback(null, { path: result[0], fd: result[1] });
const path = result[0];
const fd = result[1];
const fullPath = pathModule.resolve(cwd, BufferIsBuffer(path) ? BufferToString(path) : path);

const remove = () => {
try {
binding.close(fd);
} catch (err) {
if (err.code !== 'EBADF') throw err;
}
try {
binding.unlink(fullPath);
} catch (err) {
if (err.code !== 'ENOENT') throw err;
}
};

callback(null, {
path,
fd,
remove,
[SymbolDispose]() {
remove();
},
});
};
binding.mkstemp(prefix, options.encoding, req);
}

/**
* Synchronously creates and opens a unique temporary file.
* The returned value is an object which removes the file and closes its file
* descriptor when disposed.
* @param {string | Buffer | URL} prefix
* @param {string | { encoding?: string; }} [options]
* @returns {{ path: string | Buffer, fd: number }}
* @returns {{
* path: string | Buffer,
* fd: number,
* remove: () => void,
* [Symbol.dispose]: () => void,
* }}
*/
function mkstempSync(prefix, options) {
options = getOptions(options);
Expand All @@ -3882,7 +3921,31 @@ function mkstempSync(prefix, options) {
}

const result = binding.mkstemp(prefix, options.encoding);
return { path: result[0], fd: result[1] };
const path = result[0];
const fd = result[1];
const fullPath = pathModule.resolve(process.cwd(), BufferIsBuffer(path) ? BufferToString(path) : path);

const remove = () => {
try {
binding.close(fd);
} catch (err) {
if (err.code !== 'EBADF') throw err;
}
try {
binding.unlink(fullPath);
} catch (err) {
if (err.code !== 'ENOENT') throw err;
}
};

return {
path,
fd,
remove,
[SymbolDispose]() {
remove();
},
};
}

/**
Expand Down
37 changes: 36 additions & 1 deletion lib/internal/fs/promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -2136,6 +2136,19 @@ async function mkdtemp(prefix, options) {
);
}

/**
* Creates and opens a unique temporary file asynchronously.
* The returned value is an async-disposable object which removes the file and
* closes its file handle when disposed.
* @param {string | Buffer | URL} prefix
* @param {string | { encoding?: string; }} [options]
* @returns {Promise<{
* path: string | Buffer,
* handle: FileHandle,
* remove: () => Promise<void>,
* [Symbol.asyncDispose]: () => Promise<void>,
* }>}
*/
async function mkstemp(prefix, options) {
options = getOptions(options);
if (BufferIsBuffer(prefix)) {
Expand All @@ -2150,12 +2163,34 @@ async function mkstemp(prefix, options) {
if (promise !== undefined) return await promise;
}

const cwd = process.cwd();
const result = await PromisePrototypeThen(
binding.mkstemp(prefix, options.encoding, kUsePromises),
undefined,
handleErrorFromBinding,
);
return { __proto__: null, path: result[0], handle: new FileHandle(result[1]) };
const path = result[0];
const handle = new FileHandle(result[1]);
const fullPath = pathModule.resolve(cwd, BufferIsBuffer(path) ? BufferToString(path) : path);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not use that, it's fragile (Buffers are not always serializable to UTF-8) and unnecessary: we can use result[1]


const remove = async () => {
await handle.close();
try {
await unlink(fullPath);
} catch (err) {
if (err.code !== 'ENOENT') throw err;
}
};

return {
__proto__: null,
path,
handle,
remove,
async [SymbolAsyncDispose]() {
await remove();
},
Comment on lines +2190 to +2192

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
async [SymbolAsyncDispose]() {
await remove();
},
[SymbolAsyncDispose]: remove,

};
}

async function mkdtempDisposable(prefix, options) {
Expand Down
49 changes: 45 additions & 4 deletions lib/internal/vfs/file_system.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const {
StringPrototypeSlice,
StringPrototypeStartsWith,
Symbol,
SymbolAsyncDispose,
SymbolDispose,
} = primordials;

Expand Down Expand Up @@ -620,12 +621,38 @@ class VirtualFileSystem {
/**
* Creates and opens a unique temporary file synchronously.
* @param {string} prefix The prefix for the temp file
* @returns {{ path: string, fd: number }} The full path and a file descriptor
* @returns {{
* path: string,
* fd: number,
* remove: () => void,
* [Symbol.dispose]: () => void,
* }} The full path and a file descriptor
*/
mkstempSync(prefix) {
const filePath = this.#toProviderPrefix(prefix) + randomSuffix();
const handle = this[kProvider].openSync(filePath, 'wx+', 0o600);
return { path: this.#toMountedPath(filePath), fd: openVirtualFd(handle) };
const mountedPath = this.#toMountedPath(filePath);
const fd = openVirtualFd(handle);
const remove = () => {
try {
this.closeSync(fd);
} catch (err) {
if (err.code !== 'EBADF') throw err;
}
try {
this.unlinkSync(mountedPath);
} catch (err) {
if (err.code !== 'ENOENT') throw err;
}
};
return {
path: mountedPath,
fd,
remove,
[SymbolDispose]() {
remove();
},
};
}

/**
Expand Down Expand Up @@ -1349,10 +1376,24 @@ class VirtualFileSystem {
async mkstemp(prefix) {
const filePath = toProviderPrefix(prefix) + randomSuffix();
const handle = provider.openSync(filePath, 'wx+', 0o600);
const mountedPath = toMountedPath(filePath);
const fd = openVirtualFd(handle);
const remove = async () => {
closeVirtualFd(fd);
try {
await this.unlink(mountedPath);
} catch (err) {
if (err.code !== 'ENOENT') throw err;
}
};
return {
__proto__: null,
path: toMountedPath(filePath),
fd: openVirtualFd(handle),
path: mountedPath,
fd,
remove,
async [SymbolAsyncDispose]() {
await remove();
},
};
},

Expand Down
Loading