Skip to content

Commit

Permalink
feat(fs/unstable): add makeTempDir and makeTempDirSync (#6391)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbronder authored Feb 12, 2025
1 parent e52dc6b commit b7c76d5
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 0 deletions.
1 change: 1 addition & 0 deletions _tools/node_test_runner/run_test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import "../../collections/unzip_test.ts";
import "../../collections/without_all_test.ts";
import "../../collections/zip_test.ts";
import "../../fs/unstable_link_test.ts";
import "../../fs/unstable_make_temp_dir_test.ts";
import "../../fs/unstable_read_dir_test.ts";
import "../../fs/unstable_read_link_test.ts";
import "../../fs/unstable_real_path_test.ts";
Expand Down
14 changes: 14 additions & 0 deletions fs/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,17 @@ function checkWindows(): boolean {
export function getNodeFs() {
return (globalThis as any).process.getBuiltinModule("node:fs");
}

/**
* @returns The Node.js `os` module.
*/
export function getNodeOs() {
return (globalThis as any).process.getBuiltinModule("node:os");
}

/**
* @returns The Node.js `path` module.
*/
export function getNodePath() {
return (globalThis as any).process.getBuiltinModule("node:path");
}
1 change: 1 addition & 0 deletions fs/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"./unstable-chmod": "./unstable_chmod.ts",
"./unstable-link": "./unstable_link.ts",
"./unstable-lstat": "./unstable_lstat.ts",
"./unstable-make-temp-dir": "./unstable_make_temp_dir.ts",
"./unstable-read-dir": "./unstable_read_dir.ts",
"./unstable-read-link": "./unstable_read_link.ts",
"./unstable-real-path": "./unstable_real_path.ts",
Expand Down
151 changes: 151 additions & 0 deletions fs/unstable_make_temp_dir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright 2018-2025 the Deno authors. MIT license.

import { getNodeFs, getNodeOs, getNodePath, isDeno } from "./_utils.ts";
import type { MakeTempOptions } from "./unstable_types.ts";
import { mapError } from "./_map_error.ts";

/**
* Creates a new temporary directory in the default directory for temporary
* files, unless `dir` is specified. Other optional options include
* prefixing and suffixing the directory name with `prefix` and `suffix`
* respectively.
*
* This call resolves to the full path to the newly created directory.
*
* Multiple programs calling this function simultaneously will create different
* directories. It is the caller's responsibility to remove the directory when
* no longer needed.
*
* Requires `allow-write` permission.
*
* @example Usage
* ```ts ignore
* import { makeTempDir } from "@std/unstable-make-temp-dir";
* const tempDirName0 = await makeTempDir(); // e.g. /tmp/2894ea76
* const tempDirName1 = await makeTempDir({ prefix: 'my_temp' }); // e.g. /tmp/my_temp339c944d
* ```
*
* @tags allow-write
*
* @param options The options specified when creating a temporary directory.
* @returns A promise that resolves to a path to the temporary directory.
*/
export async function makeTempDir(options?: MakeTempOptions): Promise<string> {
if (isDeno) {
return Deno.makeTempDir({ ...options });
} else {
const {
dir = undefined,
prefix = undefined,
suffix = undefined,
} = options ?? {};

try {
const { mkdtemp, rename } = getNodeFs().promises;
const { tmpdir } = getNodeOs();
const { join, sep } = getNodePath();

if (!options) {
return await mkdtemp(join(tmpdir(), sep));
}

let prependPath = tmpdir();
if (dir != null) {
prependPath = typeof dir === "string" ? dir : ".";
if (prependPath === "") {
prependPath = ".";
}
}

if (prefix != null && typeof prefix === "string") {
prependPath = join(prependPath, prefix || sep);
} else {
prependPath = join(prependPath, sep);
}

if (suffix != null && typeof suffix === "string") {
const tempPath = await mkdtemp(prependPath);
const combinedTempPath = "".concat(tempPath, suffix);
await rename(tempPath, combinedTempPath);
return combinedTempPath;
}

return await mkdtemp(prependPath);
} catch (error) {
throw mapError(error);
}
}
}

/**
* Synchronously creates a new temporary directory in the default directory
* for temporary files, unless `dir` is specified. Other optional options
* include prefixing and suffixing the directory name with `prefix` and
* `suffix` respectively.
*
* The full path to the newly created directory is returned.
*
* Multiple programs calling this function simultaneously will create different
* directories. It is the caller's responsibility to remove the directory when
* no longer needed.
*
* Requires `allow-write` permission.
*
* @example Usage
* ```ts ignore
* import { makeTempDirSync } from "@std/fs/unstable-make-temp-dir";
* const tempDirName0 = makeTempDirSync(); // e.g. /tmp/2894ea76
* const tempDirName1 = makeTempDirSync({ prefix: 'my_temp' }); // e.g. /tmp/my_temp339c944d
* ```
*
* @tags allow-write
*
* @param options The options specified when creating a temporary directory.
* @returns The path of the temporary directory.
*/
export function makeTempDirSync(options?: MakeTempOptions): string {
if (isDeno) {
return Deno.makeTempDirSync({ ...options });
} else {
const {
dir = undefined,
prefix = undefined,
suffix = undefined,
} = options ?? {};

try {
const { mkdtempSync, renameSync } = getNodeFs();
const { tmpdir } = getNodeOs();
const { join, sep } = getNodePath();

if (!options) {
return mkdtempSync(join(tmpdir(), sep));
}

let prependPath = tmpdir();
if (dir != null) {
prependPath = typeof dir === "string" ? dir : ".";
if (prependPath === "") {
prependPath = ".";
}
}

if (prefix != null && typeof prefix === "string") {
prependPath = join(prependPath, prefix || sep);
} else {
prependPath = join(prependPath, sep);
}

if (suffix != null && typeof prefix === "string") {
const tempPath = mkdtempSync(prependPath);
const combinedTempPath = "".concat(tempPath, suffix);
renameSync(tempPath, combinedTempPath);
return combinedTempPath;
}

return mkdtempSync(prependPath);
} catch (error) {
throw mapError(error);
}
}
}
79 changes: 79 additions & 0 deletions fs/unstable_make_temp_dir_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright 2018-2025 the Deno authors. MIT license.

import { assert, assertRejects, assertThrows } from "@std/assert";
import { makeTempDir, makeTempDirSync } from "./unstable_make_temp_dir.ts";
import { NotFound } from "./unstable_errors.js";
import { rmSync } from "node:fs";
import { rm } from "node:fs/promises";

Deno.test("makeTempDir() creates temporary directories in the default temp directory path", async () => {
const dir1 = await makeTempDir({ prefix: "standard", suffix: "library" });
const dir2 = await makeTempDir({ prefix: "standard", suffix: "library" });

try {
assert(dir1 !== dir2);

for (const dir of [dir1, dir2]) {
const tempDirName = dir.replace(/^.*[\\\/]/, "");
assert(tempDirName.startsWith("standard"));
assert(tempDirName.endsWith("library"));
}
} finally {
await rm(dir1, { recursive: true, force: true });
await rm(dir2, { recursive: true, force: true });
}
});

Deno.test("makeTempDir() creates temporary directories with the 'dir' option", async () => {
const tempParent = await makeTempDir({ prefix: "first", suffix: "last" });
const dir = await makeTempDir({ dir: tempParent });

try {
assert(dir.startsWith(tempParent));
assert(/^[\\\/]/.test(dir.slice(tempParent.length)));
} finally {
await rm(tempParent, { recursive: true, force: true });
}
});

Deno.test("makeTempDir() rejects with NotFound when passing a 'dir' path that does not exist", async () => {
await assertRejects(async () => {
await makeTempDir({ dir: "/non-existent-dir" });
}, NotFound);
});

Deno.test("makeTempDirSync() creates temporary directories in the default temp directory path", () => {
const dir1 = makeTempDirSync({ prefix: "standard", suffix: "library" });
const dir2 = makeTempDirSync({ prefix: "standard", suffix: "library" });

try {
assert(dir1 !== dir2);

for (const dir of [dir1, dir2]) {
const tempDirName = dir.replace(/^.*[\\\/]/, "");
assert(tempDirName.startsWith("standard"));
assert(tempDirName.endsWith("library"));
}
} finally {
rmSync(dir1, { recursive: true, force: true });
rmSync(dir2, { recursive: true, force: true });
}
});

Deno.test("makeTempDirSync() creates temporary directories with the 'dir' option", () => {
const tempParent = makeTempDirSync({ prefix: "first", suffix: "last" });
const dir = makeTempDirSync({ dir: tempParent });

try {
assert(dir.startsWith(tempParent));
assert(/^[\\\/]/.test(dir.slice(tempParent.length)));
} finally {
rmSync(tempParent, { recursive: true, force: true });
}
});

Deno.test("makeTempDirSync() throws with NotFound when passing a 'dir' path that does not exist", () => {
assertThrows(() => {
makeTempDirSync({ dir: "/non-existent-dir" });
}, NotFound);
});
27 changes: 27 additions & 0 deletions fs/unstable_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,30 @@ export interface SymlinkOptions {
* option only applies to Windows and is ignored on other operating systems. */
type: "file" | "dir" | "junction";
}

/**
* Options which can be set when using {@linkcode makeTempDir},
* {@linkcode makeTempDirSync}, {@linkcode makeTempFile}, and
* {@linkcode makeTempFileSync}.
*/
export interface MakeTempOptions {
/**
* Directory where the temporary directory should be created (defaults to the
* env variable `TMPDIR`, or the system's default, usually `/tmp`).
*
* Note that if the passed `dir` is relative, the path returned by
* `makeTempFile()` and `makeTempDir()` will also be relative. Be mindful of
* this when changing working directory.
*/
dir?: string;
/**
* String that should precede the random portion of the temporary directory's
* name.
*/
prefix?: string;
/**
* String that should follow the random portion of the temporary directory's
* name.
*/
suffix?: string;
}

0 comments on commit b7c76d5

Please sign in to comment.