Skip to content

Commit

Permalink
feat(fs/unstable): add rename (#6379)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbronder authored Feb 4, 2025
1 parent 1f032bb commit 3b75ee7
Show file tree
Hide file tree
Showing 4 changed files with 239 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 @@ -53,6 +53,7 @@ import "../../fs/unstable_link_test.ts";
import "../../fs/unstable_read_dir_test.ts";
import "../../fs/unstable_read_link_test.ts";
import "../../fs/unstable_real_path_test.ts";
import "../../fs/unstable_rename_test.ts";
import "../../fs/unstable_stat_test.ts";
import "../../fs/unstable_symlink_test.ts";
import "../../fs/unstable_lstat_test.ts";
Expand Down
1 change: 1 addition & 0 deletions fs/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"./unstable-read-dir": "./unstable_read_dir.ts",
"./unstable-read-link": "./unstable_read_link.ts",
"./unstable-real-path": "./unstable_real_path.ts",
"./unstable-rename": "./unstable_rename.ts",
"./unstable-stat": "./unstable_stat.ts",
"./unstable-symlink": "./unstable_symlink.ts",
"./unstable-types": "./unstable_types.ts",
Expand Down
44 changes: 44 additions & 0 deletions fs/unstable_rename.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2018-2025 the Deno authors. MIT license.

import { getNodeFs, isDeno } from "./_utils.ts";
import { mapError } from "./_map_error.ts";

/**
* Renames (moves) `oldpath` to `newpath`. Paths may be files or directories.
* If `newpath` already exists and is not a directory, `rename()` replaces it.
* OS-specific restrictions may apply when `oldpath` and `newpath` are in
* different directories.
*
* On Unix-like OSes, this operation does not follow symlinks at either path.
*
* It varies between platforms when the operation throws errors, and if so
* what they are. It's always an error to rename anything to a non-empty
* directory.
*
* Requires `allow-read` and `allow-write` permissions.
*
* @example Usage
* ```ts ignore
* import { rename } from "@std/fs/unstable-rename";
* await rename("old/path", "new/path");
* ```
*
* @tags allow-read, allow-write
*
* @param oldpath The current name/path of the file/directory.
* @param newpath The updated name/path of the file/directory.
*/
export async function rename(
oldpath: string | URL,
newpath: string | URL,
): Promise<void> {
if (isDeno) {
await Deno.rename(oldpath, newpath);
} else {
try {
await getNodeFs().promises.rename(oldpath, newpath);
} catch (error) {
throw mapError(error);
}
}
}
193 changes: 193 additions & 0 deletions fs/unstable_rename_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// Copyright 2018-2025 the Deno authors. MIT license.

import { assert, assertRejects } from "@std/assert";
import { rename } from "./unstable_rename.ts";
import { NotFound } from "./unstable_errors.js";
import { lstatSync } from "node:fs";
import { mkdir, mkdtemp, open, rm, stat, symlink } from "node:fs/promises";
import { platform, tmpdir } from "node:os";
import { join, resolve } from "node:path";

/** Tests if the original file/directory is missing since the file is renamed.
* Uses Node.js Error instances to check because the `lstatSync` function is
* pulled in from the `node:fs` package without using `mapError`. */
function assertMissing(path: string) {
let caughtErr = false;
let info;
try {
info = lstatSync(path);
} catch (error) {
caughtErr = true;
// Check if the error caught is a Node.js error instance.
if (error instanceof Error && "code" in error) {
assert(error.code === "ENOENT", "errno code is not ENOENT.");
}
}
assert(caughtErr);
assert(info === undefined);
}

Deno.test("rename() renames a regular file", async () => {
const tempDirPath = await mkdtemp(resolve(tmpdir(), "rename_"));
const testFile = join(tempDirPath, "testFile.txt");
const renameFile = join(tempDirPath, "renamedFile.txt");

const testFh = await open(testFile, "w");
await testFh.close();

await rename(testFile, renameFile);
assertMissing(testFile);
const renameFileStat = await stat(renameFile);
assert(renameFileStat.isFile());

await rm(tempDirPath, { recursive: true, force: true });
});

Deno.test("rename() rejects with Error when an existing regular file is renamed with an existing directory path", async () => {
const tempDirPath = await mkdtemp(resolve(tmpdir(), "rename_"));
const testFile = join(tempDirPath, "testFile.txt");
const testDir = join(tempDirPath, "testDir");

const tempFh = await open(testFile, "w");
await tempFh.close();
await mkdir(testDir);

await assertRejects(async () => {
await rename(testFile, testDir);
}, Error);

await rm(tempDirPath, { recursive: true, force: true });
});

Deno.test("rename() rejects with Error when an existing directory is renamed with an existing directory containing a file", async () => {
const tempDirPath = await mkdtemp(resolve(tmpdir(), "rename_"));
const emptyDir = join(tempDirPath, "emptyDir");
const fullDir = join(tempDirPath, "fullDir");
const testFile = join(fullDir, "testFile.txt");

await mkdir(fullDir);
await mkdir(emptyDir);
const testFh = await open(testFile, "w");
await testFh.close();

await assertRejects(async () => {
await rename(emptyDir, fullDir);
}, Error);

await rm(tempDirPath, { recursive: true, force: true });
});

Deno.test("rename() rejects with Error on Windows and succeeds on *nix when an existing directory is renamed with another directory path", async () => {
const tempDirPath = await mkdtemp(resolve(tmpdir(), "rename_"));
const testDir = join(tempDirPath, "testDir");
const anotherDir = join(tempDirPath, "anotherDir");

await mkdir(testDir);
await mkdir(anotherDir);

if (platform() === "win32") {
await assertRejects(async () => {
await rename(testDir, anotherDir);
}, Error);
} else {
await rename(testDir, anotherDir);
assertMissing(testDir);
const anotherDirStat = await stat(anotherDir);
assert(anotherDirStat.isDirectory());
}

await rm(tempDirPath, { recursive: true, force: true });
});

Deno.test("rename() rejects with Error on *nix and succeeds on Windows when an existing directory is renamed with an existing regular file path", async () => {
const tempDirPath = await mkdtemp(resolve(tmpdir(), "rename_"));
const testFile = join(tempDirPath, "testFile.txt");
const testDir = join(tempDirPath, "testDir");

const testFh = await open(testFile, "w");
await testFh.close();
await mkdir(testDir);

if (platform() === "win32") {
await rename(testDir, testFile);
const fileStat = await stat(testFile);
assert(fileStat.isDirectory());
} else {
await assertRejects(async () => {
await rename(testDir, testFile);
}, Error);
}

await rm(tempDirPath, { recursive: true, force: true });
});

Deno.test({
name:
"rename() rejects with Error when renaming an existing directory with a valid symlink'd regular file path",
ignore: platform() === "win32",
fn: async () => {
const tempDirPath = await mkdtemp(resolve(tmpdir(), "rename_"));
const testDir = join(tempDirPath, "testDir");
const testFile = join(tempDirPath, "testFile.txt");
const symlinkFile = join(tempDirPath, "testFile.txt.link");

await mkdir(testDir);
const testFh = await open(testFile, "w");
await testFh.close();
await symlink(testFile, symlinkFile);

await assertRejects(async () => {
await rename(testDir, symlinkFile);
}, Error);

await rm(tempDirPath, { recursive: true, force: true });
},
});

Deno.test({
name:
"rename() rejects with Error when renaming an existing directory with a valid symlink'd directory path",
ignore: platform() === "win32",
fn: async () => {
const tempDirPath = await mkdtemp(resolve(tmpdir(), "rename_"));
const testDir = join(tempDirPath, "testDir");
const anotherDir = join(tempDirPath, "anotherDir");
const symlinkDir = join(tempDirPath, "symlinkDir");

await mkdir(testDir);
await mkdir(anotherDir);
await symlink(anotherDir, symlinkDir);

await assertRejects(async () => {
await rename(testDir, symlinkDir);
}, Error);

await rm(tempDirPath, { recursive: true, force: true });
},
});

Deno.test({
name:
"rename() rejects with Error when renaming an existing directory with a symlink'd file pointing to a non-existent file path",
ignore: platform() === "win32",
fn: async () => {
const tempDirPath = await mkdtemp(resolve(tmpdir(), "rename_"));
const testDir = join(tempDirPath, "testDir");
const symlinkPath = join(tempDirPath, "symlinkPath");

await mkdir(testDir);
await symlink("non-existent", symlinkPath);

await assertRejects(async () => {
await rename(testDir, symlinkPath);
}, Error);

await rm(tempDirPath, { recursive: true, force: true });
},
});

Deno.test("rename() rejects with NotFound for renaming a non-existent file", async () => {
await assertRejects(async () => {
await rename("non-existent-file.txt", "new-name.txt");
}, NotFound);
});

0 comments on commit 3b75ee7

Please sign in to comment.