Skip to content

Commit 0107c8d

Browse files
committed
Add parentDirectory option
Fixes #46
1 parent 6dfd243 commit 0107c8d

File tree

4 files changed

+127
-10
lines changed

4 files changed

+127
-10
lines changed

index.d.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
import {type Buffer} from 'node:buffer';
22
import {type MergeExclusive, type TypedArray} from 'type-fest';
33

4+
export type BaseOptions = {
5+
/**
6+
The name of a directory inside the OS temporary directory to create the temporary file or directory in. The directory is created automatically if it doesn't exist.
7+
8+
By default, temporary files and directories are created directly inside the OS temporary directory. This option lets you group related temporary files into a subdirectory.
9+
10+
Useful for organizing temporary files by app or task, making cleanup and debugging easier.
11+
12+
@example
13+
```
14+
import {temporaryFile} from 'tempy';
15+
16+
temporaryFile({parentDirectory: 'my-app'});
17+
//=> '/private/var/folders/3x/jf5977fn79jbglr7rk0tq4d00000gn/T/my-app/4f504b9edb5ba0e89451617bf9f971dd'
18+
```
19+
*/
20+
readonly parentDirectory?: string;
21+
};
22+
423
export type FileOptions = MergeExclusive<
524
{
625
/**
@@ -22,7 +41,7 @@ export type FileOptions = MergeExclusive<
2241
*/
2342
readonly name?: string;
2443
}
25-
>;
44+
> & BaseOptions;
2645

2746
export type DirectoryOptions = {
2847
/**
@@ -33,7 +52,7 @@ export type DirectoryOptions = {
3352
Useful for testing by making it easier to identify cache directories that are created.
3453
*/
3554
readonly prefix?: string;
36-
};
55+
} & BaseOptions;
3756

3857
/**
3958
The temporary path created by the function. Can be asynchronous.

index.js

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,23 @@ function assertSafePathComponent(pathComponent) {
2929
}
3030
}
3131

32-
const getPath = (prefix = '') => {
32+
function resolveParentDirectory(parentDirectory) {
33+
assertSafePathComponent(parentDirectory);
34+
35+
const resolved = path.join(tempDir, parentDirectory);
36+
fs.mkdirSync(resolved, {recursive: true});
37+
38+
return resolved;
39+
}
40+
41+
const getPath = (prefix = '', parentDirectory) => {
3342
if (prefix) {
3443
assertSafePathComponent(prefix);
3544
}
3645

37-
return path.join(tempDir, prefix + uniqueString());
46+
const parent = parentDirectory ? resolveParentDirectory(parentDirectory) : tempDir;
47+
48+
return path.join(parent, prefix + uniqueString());
3849
};
3950

4051
const writeStream = async (filePath, data) => pipeline(data, fs.createWriteStream(filePath));
@@ -47,29 +58,29 @@ async function runTask(temporaryPath, callback) {
4758
}
4859
}
4960

50-
export function temporaryFile({name, extension} = {}) {
61+
export function temporaryFile({name, extension, parentDirectory} = {}) {
5162
if (name !== undefined && name !== null) {
5263
if (extension !== undefined && extension !== null) {
5364
throw new Error('The `name` and `extension` options are mutually exclusive');
5465
}
5566

5667
assertSafePathComponent(name);
5768

58-
return path.join(temporaryDirectory(), name);
69+
return path.join(temporaryDirectory({parentDirectory}), name);
5970
}
6071

6172
if (extension !== undefined && extension !== null) {
6273
assertSafePathComponent(extension);
6374
}
6475

65-
return getPath() + (extension === undefined || extension === null ? '' : '.' + extension.replace(/^\./, ''));
76+
return getPath('', parentDirectory) + (extension === undefined || extension === null ? '' : '.' + extension.replace(/^\./, ''));
6677
}
6778

6879
export const temporaryFileTask = async (callback, options) => runTask(temporaryFile(options), callback);
6980

70-
export function temporaryDirectory({prefix = ''} = {}) {
71-
const directory = getPath(prefix);
72-
fs.mkdirSync(directory);
81+
export function temporaryDirectory({prefix = '', parentDirectory} = {}) {
82+
const directory = getPath(prefix, parentDirectory);
83+
fs.mkdirSync(directory, {recursive: true});
7384
return directory;
7485
}
7586

readme.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,23 @@ Type: `string`
6363

6464
Filename. Mutually exclusive with the `extension` option.
6565

66+
##### parentDirectory
67+
68+
Type: `string`
69+
70+
The name of a directory inside the OS temporary directory to create the temporary file in. The directory is created automatically if it doesn't exist.
71+
72+
By default, the temporary file is created directly inside the OS temporary directory. This option lets you group related temporary files into a subdirectory.
73+
74+
Useful for organizing temporary files by app or task, making cleanup and debugging easier.
75+
76+
```js
77+
import {temporaryFile} from 'tempy';
78+
79+
temporaryFile({parentDirectory: 'my-app'});
80+
//=> '/private/var/folders/3x/jf5977fn79jbglr7rk0tq4d00000gn/T/my-app/4f504b9edb5ba0e89451617bf9f971dd'
81+
```
82+
6683
### temporaryDirectory(options?)
6784

6885
Get a temporary directory path. The directory is created for you.
@@ -91,6 +108,21 @@ Useful for testing by making it easier to identify cache directories that are cr
91108

92109
*You usually won't need this option. Specify it only when actually needed.*
93110

111+
##### parentDirectory
112+
113+
Type: `string`
114+
115+
The name of a directory inside the OS temporary directory to create the temporary directory in. The directory is created automatically if it doesn't exist.
116+
117+
By default, the temporary directory is created directly inside the OS temporary directory. This option lets you group related temporary directories into a subdirectory.
118+
119+
```js
120+
import {temporaryDirectory} from 'tempy';
121+
122+
temporaryDirectory({parentDirectory: 'my-app'});
123+
//=> '/private/var/folders/3x/jf5977fn79jbglr7rk0tq4d00000gn/T/my-app/4f504b9edb5ba0e89451617bf9f971dd'
124+
```
125+
94126
### temporaryWrite(fileContent, options?)
95127

96128
Write data to a random temp file.

test.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,61 @@ test('.root', t => {
141141
t.true(path.isAbsolute(rootTemporaryDirectory));
142142
});
143143

144+
test('.file() with parentDirectory', t => {
145+
const filePath = temporaryFile({parentDirectory: 'my-app'});
146+
t.true(filePath.includes(path.join(tempDir, 'my-app')));
147+
});
148+
149+
test('.file() with parentDirectory and extension', t => {
150+
const filePath = temporaryFile({parentDirectory: 'my-app', extension: 'png'});
151+
t.true(filePath.includes(path.join(tempDir, 'my-app')));
152+
t.true(filePath.endsWith('.png'));
153+
});
154+
155+
test('.file() with parentDirectory and name', t => {
156+
const filePath = temporaryFile({parentDirectory: 'my-app', name: 'unicorn.png'});
157+
t.true(filePath.includes(path.join(tempDir, 'my-app')));
158+
t.is(path.basename(filePath), 'unicorn.png');
159+
});
160+
161+
test('.directory() with parentDirectory', t => {
162+
const directory = temporaryDirectory({parentDirectory: 'my-app'});
163+
t.true(directory.includes(path.join(tempDir, 'my-app')));
164+
t.true(fs.statSync(directory).isDirectory());
165+
});
166+
167+
test('.directory() with parentDirectory and prefix', t => {
168+
const directory = temporaryDirectory({parentDirectory: 'my-app', prefix: 'name_'});
169+
t.true(directory.includes(path.join(tempDir, 'my-app')));
170+
t.true(path.basename(directory).startsWith('name_'));
171+
});
172+
173+
test('.file() with parentDirectory - auto-creates the directory', t => {
174+
const parentDirectory = `auto-create-name-${Date.now()}`;
175+
const filePath = temporaryFile({parentDirectory, name: 'test.txt'});
176+
t.true(fs.existsSync(path.dirname(filePath)));
177+
});
178+
179+
test('.file() with parentDirectory and extension - auto-creates the directory', t => {
180+
const parentDirectory = `auto-create-ext-${Date.now()}`;
181+
const filePath = temporaryFile({parentDirectory, extension: 'png'});
182+
t.true(fs.existsSync(path.dirname(filePath)));
183+
t.true(filePath.endsWith('.png'));
184+
});
185+
186+
test('.write() with parentDirectory', async t => {
187+
const parentDirectory = `auto-create-write-${Date.now()}`;
188+
const filePath = await temporaryWrite('unicorn', {parentDirectory});
189+
t.is(fs.readFileSync(filePath, 'utf8'), 'unicorn');
190+
t.true(filePath.includes(path.join(tempDir, parentDirectory)));
191+
});
192+
193+
test('.file() with parentDirectory - rejects unsafe values', t => {
194+
for (const fixture of ['..', '../foo', 'foo/bar', 'foo\\bar', 'foo\0bar']) {
195+
t.throws(() => temporaryFile({parentDirectory: fixture}), {message: /Unsafe path component/});
196+
}
197+
});
198+
144199
// TODO: Use `unsafeFilenameFixtures` from `is-safe-filename` when targeting Node.js 20.
145200
const unsafeFilenameFixtures = [
146201
'',

0 commit comments

Comments
 (0)