-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
fs: add disposable mkdtemp #58516
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fs: add disposable mkdtemp #58516
Changes from 14 commits
5f9db1e
55fffe7
ca15b36
3a3e545
c372931
9295579
33dec17
b3b4771
5a9282c
c27abc2
82a21a3
530dfc8
8dc0d54
d216f6d
611d847
93b1e12
ed2c3cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1308,6 +1308,37 @@ characters directly to the `prefix` string. For instance, given a directory | |
`prefix` must end with a trailing platform-specific path separator | ||
(`require('node:path').sep`). | ||
|
||
### `fsPromises.mkdtempDisposable(prefix[, options])` | ||
|
||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
* `prefix` {string|Buffer|URL} | ||
* `options` {string|Object} | ||
* `encoding` {string} **Default:** `'utf8'` | ||
* Returns: {Promise} Fulfills with a Promise for an async-disposable Object: | ||
* `path` {string} The path of the created directory. | ||
* `remove` {AsyncFunction} A function which removes the created directory. | ||
* `[Symbol.asyncDispose]` {AsyncFunction} The same as `remove`. | ||
Comment on lines
+1320
to
+1323
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider using a pattern here like what we do with the various pseudo-classes for params/options cases in Web Crypto. https://nodejs.org/docs/latest/api/webcrypto.html#algorithm-parameters ... these aren't actual classes in the code but are documented as such to improve navigation and documentability in the docs. Essentially, while this new method is actually returning an anonymous object, it can still be documented as if it were a named class. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can do this, but where should the docs go?
which is not true of these objects - the sync and async versions return objects with a different shape, specific to that function and no other. My inclination would be to make a sub-heading within the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would just put them there in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But really it's up to you. Wherever you feel most comfortable with it. |
||
|
||
The resulting Promise holds an async-disposable object whose `path` property | ||
holds the created directory path. When the object is disposed, the directory | ||
and its contents will be removed asynchronously if it still exists. If the | ||
directory 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 should be used with `await` + `await using` as in | ||
`await using dir = await fsPromises.mkdtempDisposable('prefix')`. | ||
jasnell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
<!-- TODO: link MDN docs for disposables once https://github.com/mdn/content/pull/38027 lands --> | ||
|
||
For detailed information, see the documentation of [`fsPromises.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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This really should be expanded to indicate what the encoding is used for. This might be an existing problem in the documentation. |
||
|
||
### `fsPromises.open(path, flags[, mode])` | ||
|
||
<!-- YAML | ||
|
@@ -5902,6 +5933,36 @@ 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.mkdtempDisposableSync(prefix[, options])` | ||
|
||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
* `prefix` {string|Buffer|URL} | ||
* `options` {string|Object} | ||
* `encoding` {string} **Default:** `'utf8'` | ||
* Returns: {Object} A disposable object: | ||
* `path` {string} The path of the created directory. | ||
* `remove` {Function} A function which removes the created directory. | ||
* `[Symbol.dispose]` {Function} The same as `remove`. | ||
|
||
Returns a disposable object whose `path` property holds the created directory | ||
path. When the object is disposed, the directory and its contents will be | ||
removed if it still exists. If the directory cannot be deleted, disposal will | ||
throw an error. The object has a `remove()` method which will perform the same | ||
task. | ||
|
||
<!-- TODO: link MDN docs for disposables once https://github.com/mdn/content/pull/38027 lands --> | ||
|
||
For detailed information, see the documentation of [`fs.mkdtemp()`][]. | ||
|
||
There is no callback-based version of this API because it is designed for use | ||
with the `using` syntax. | ||
|
||
The optional `options` argument can be a string specifying an encoding, or an | ||
object with an `encoding` property specifying the character encoding to use. | ||
bakkot marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
### `fs.opendirSync(path[, options])` | ||
|
||
<!-- YAML | ||
|
@@ -8494,6 +8555,7 @@ the file contents. | |
[`fs.writev()`]: #fswritevfd-buffers-position-callback | ||
[`fsPromises.access()`]: #fspromisesaccesspath-mode | ||
[`fsPromises.copyFile()`]: #fspromisescopyfilesrc-dest-mode | ||
[`fsPromises.mkdtemp()`]: #fspromisesmkdtempprefix-options | ||
[`fsPromises.open()`]: #fspromisesopenpath-flags-mode | ||
[`fsPromises.opendir()`]: #fspromisesopendirpath-options | ||
[`fsPromises.rm()`]: #fspromisesrmpath-options | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
'use strict'; | ||
|
||
const common = require('../common'); | ||
const assert = require('assert'); | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
|
||
const tmpdir = require('../common/tmpdir'); | ||
tmpdir.refresh(); | ||
|
||
// Basic usage | ||
{ | ||
const result = fs.mkdtempDisposableSync(tmpdir.resolve('foo.')); | ||
|
||
assert.strictEqual(path.basename(result.path).length, 'foo.XXXXXX'.length); | ||
assert.strictEqual(path.dirname(result.path), tmpdir.path); | ||
assert(fs.existsSync(result.path)); | ||
|
||
result.remove(); | ||
|
||
assert(!fs.existsSync(result.path)); | ||
|
||
// Second removal does not throw error | ||
result.remove(); | ||
} | ||
|
||
// Usage with [Symbol.dispose]() | ||
{ | ||
const result = fs.mkdtempDisposableSync(tmpdir.resolve('foo.')); | ||
|
||
assert(fs.existsSync(result.path)); | ||
|
||
result[Symbol.dispose](); | ||
|
||
assert(!fs.existsSync(result.path)); | ||
|
||
// Second removal does not throw error | ||
result[Symbol.dispose](); | ||
} | ||
|
||
// `chdir`` does not affect removal | ||
{ | ||
const originalCwd = process.cwd(); | ||
|
||
process.chdir(tmpdir.path); | ||
const first = fs.mkdtempDisposableSync('first.'); | ||
const second = fs.mkdtempDisposableSync('second.'); | ||
|
||
const fullFirstPath = path.join(tmpdir.path, first.path); | ||
const fullSecondPath = path.join(tmpdir.path, second.path); | ||
|
||
assert(fs.existsSync(fullFirstPath)); | ||
assert(fs.existsSync(fullSecondPath)); | ||
|
||
process.chdir(fullFirstPath); | ||
second.remove(); | ||
|
||
assert(!fs.existsSync(fullSecondPath)); | ||
|
||
process.chdir(tmpdir.path); | ||
first.remove(); | ||
assert(!fs.existsSync(fullFirstPath)); | ||
|
||
process.chdir(originalCwd); | ||
} | ||
|
||
// Errors from cleanup are thrown | ||
{ | ||
const base = fs.mkdtempDisposableSync(tmpdir.resolve('foo.')); | ||
|
||
if (common.isWindows) { | ||
// On Windows we can prevent removal by holding a file open | ||
const testFile = path.join(base.path, 'locked-file.txt'); | ||
fs.writeFileSync(testFile, 'test'); | ||
const fd = fs.openSync(testFile, 'r'); | ||
|
||
assert.throws(() => { | ||
base.remove(); | ||
}, /EBUSY|ENOTEMPTY|EPERM/); | ||
|
||
fs.closeSync(fd); | ||
fs.unlinkSync(testFile); | ||
|
||
// Removal works once file is closed | ||
base.remove(); | ||
assert(!fs.existsSync(base.path)); | ||
} else { | ||
// On Unix we can prevent removal by making the parent directory read-only | ||
const child = fs.mkdtempDisposableSync(path.join(base.path, 'bar.')); | ||
|
||
const originalMode = fs.statSync(base.path).mode; | ||
fs.chmodSync(base.path, 0o444); | ||
|
||
assert.throws(() => { | ||
child.remove(); | ||
}, /EACCES|EPERM/); | ||
|
||
fs.chmodSync(base.path, originalMode); | ||
|
||
// Removal works once permissions are reset | ||
child.remove(); | ||
assert(!fs.existsSync(child.path)); | ||
|
||
base.remove(); | ||
assert(!fs.existsSync(base.path)); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.