diff --git a/_tools/node_test_runner/run_test.mjs b/_tools/node_test_runner/run_test.mjs index b15cdaeddde8..cebb60fc67c8 100644 --- a/_tools/node_test_runner/run_test.mjs +++ b/_tools/node_test_runner/run_test.mjs @@ -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"; diff --git a/fs/_utils.ts b/fs/_utils.ts index 76478802f5d2..d8c4593e02dd 100644 --- a/fs/_utils.ts +++ b/fs/_utils.ts @@ -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"); +} diff --git a/fs/deno.json b/fs/deno.json index bf24e9531e2e..1cb8a4618672 100644 --- a/fs/deno.json +++ b/fs/deno.json @@ -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", diff --git a/fs/unstable_make_temp_dir.ts b/fs/unstable_make_temp_dir.ts new file mode 100644 index 000000000000..a8a09a243af6 --- /dev/null +++ b/fs/unstable_make_temp_dir.ts @@ -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 { + 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); + } + } +} diff --git a/fs/unstable_make_temp_dir_test.ts b/fs/unstable_make_temp_dir_test.ts new file mode 100644 index 000000000000..8c319ed98a56 --- /dev/null +++ b/fs/unstable_make_temp_dir_test.ts @@ -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); +}); diff --git a/fs/unstable_types.ts b/fs/unstable_types.ts index 60d61e8a8a3e..8ad466ce961c 100644 --- a/fs/unstable_types.ts +++ b/fs/unstable_types.ts @@ -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; +}