Skip to content

Commit 765cb08

Browse files
authored
feat(fs/unstable): add renameSync (denoland#6396)
1 parent 4cb8b3f commit 765cb08

File tree

2 files changed

+227
-3
lines changed

2 files changed

+227
-3
lines changed

fs/unstable_rename.ts

+36
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,39 @@ export async function rename(
4242
}
4343
}
4444
}
45+
46+
/**
47+
* Synchronously renames (moves) `oldpath` to `newpath`. Paths may be files or
48+
* directories. If `newpath` already exists and is not a directory,
49+
* `renameSync()` replaces it. OS-specific restrictions may apply when
50+
* `oldpath` and `newpath` are in different directories.
51+
*
52+
* On Unix-like OSes, this operation does not follow symlinks at either path.
53+
*
54+
* It varies between platforms when the operation throws errors, and if so what
55+
* they are. It's always an error to rename anything to a non-empty directory.
56+
*
57+
* Requires `allow-read` and `allow-write` permissions.
58+
*
59+
* @example Usage
60+
* ```ts ignore
61+
* import { renameSync } from "@std/fs/unstable-rename";
62+
* renameSync("old/path", "new/path");
63+
* ```
64+
*
65+
* @tags allow-read, allow-write
66+
*
67+
* @param oldpath The current name/path of the file/directory.
68+
* @param newpath The updated name/path of the file/directory.
69+
*/
70+
export function renameSync(oldpath: string | URL, newpath: string | URL): void {
71+
if (isDeno) {
72+
Deno.renameSync(oldpath, newpath);
73+
} else {
74+
try {
75+
getNodeFs().renameSync(oldpath, newpath);
76+
} catch (error) {
77+
throw mapError(error);
78+
}
79+
}
80+
}

fs/unstable_rename_test.ts

+191-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
// Copyright 2018-2025 the Deno authors. MIT license.
22

3-
import { assert, assertRejects } from "@std/assert";
4-
import { rename } from "./unstable_rename.ts";
3+
import { assert, assertRejects, assertThrows } from "@std/assert";
4+
import { rename, renameSync } from "./unstable_rename.ts";
55
import { NotFound } from "./unstable_errors.js";
6-
import { lstatSync } from "node:fs";
76
import { mkdir, mkdtemp, open, rm, stat, symlink } from "node:fs/promises";
87
import { platform, tmpdir } from "node:os";
98
import { join, resolve } from "node:path";
9+
import {
10+
closeSync,
11+
lstatSync,
12+
mkdirSync,
13+
mkdtempSync,
14+
openSync,
15+
rmSync,
16+
statSync,
17+
symlinkSync,
18+
} from "node:fs";
1019

1120
/** Tests if the original file/directory is missing since the file is renamed.
1221
* Uses Node.js Error instances to check because the `lstatSync` function is
@@ -191,3 +200,182 @@ Deno.test("rename() rejects with NotFound for renaming a non-existent file", asy
191200
await rename("non-existent-file.txt", "new-name.txt");
192201
}, NotFound);
193202
});
203+
204+
Deno.test("renameSync() renames a regular file", () => {
205+
const tempDirPath = mkdtempSync(resolve(tmpdir(), "renameSync_"));
206+
const testFile = join(tempDirPath, "testFile.txt");
207+
const renameFile = join(tempDirPath, "renamedFile.txt");
208+
209+
const testFd = openSync(testFile, "w");
210+
closeSync(testFd);
211+
212+
renameSync(testFile, renameFile);
213+
assertMissing(testFile);
214+
const renameFileStat = statSync(renameFile);
215+
assert(renameFileStat.isFile);
216+
217+
rmSync(tempDirPath, { recursive: true, force: true });
218+
});
219+
220+
Deno.test("renameSync() throws with Error when an existing regular file is renamed with an existing directory path", () => {
221+
const tempDirPath = mkdtempSync(resolve(tmpdir(), "renameSync_"));
222+
const testFile = join(tempDirPath, "testFile.txt");
223+
const testDir = join(tempDirPath, "testDir");
224+
225+
const testFd = openSync(testFile, "w");
226+
closeSync(testFd);
227+
mkdirSync(testDir);
228+
229+
assertThrows(() => {
230+
renameSync(testFile, testDir);
231+
}, Error);
232+
});
233+
234+
Deno.test("renameSync() throws with Error when an existing file path is renamed with an existing directory path", () => {
235+
const tempDirPath = mkdtempSync(resolve(tmpdir(), "renameSync_"));
236+
const testFile = join(tempDirPath, "testFile.txt");
237+
const testDir = join(tempDirPath, "testDir");
238+
239+
const testFd = openSync(testFile, "w");
240+
closeSync(testFd);
241+
mkdirSync(testDir);
242+
243+
assertThrows(() => {
244+
renameSync(testFile, testDir);
245+
}, Error);
246+
247+
rmSync(tempDirPath, { recursive: true, force: true });
248+
});
249+
250+
Deno.test("renameSync() throws with Error when an existing directory is renamed with an existing directory containing a file", () => {
251+
const tempDirPath = mkdtempSync(resolve(tmpdir(), "renameSync_"));
252+
const emptyDir = join(tempDirPath, "emptyDir");
253+
const fullDir = join(tempDirPath, "fullDir");
254+
const testFile = join(fullDir, "testFile.txt");
255+
256+
mkdirSync(fullDir);
257+
mkdirSync(emptyDir);
258+
const testFd = openSync(testFile, "w");
259+
closeSync(testFd);
260+
261+
assertThrows(() => {
262+
renameSync(emptyDir, fullDir);
263+
}, Error);
264+
265+
rmSync(tempDirPath, { recursive: true, force: true });
266+
});
267+
268+
Deno.test("renameSync() throws with Error on Windows and succeeds on *nix when an existing directory is renamed with another directory path", () => {
269+
const tempDirPath = mkdtempSync(resolve(tmpdir(), "renameSync_"));
270+
const testDir = join(tempDirPath, "testDir");
271+
const anotherDir = join(tempDirPath, "anotherDir");
272+
273+
mkdirSync(testDir);
274+
mkdirSync(anotherDir);
275+
276+
if (platform() === "win32") {
277+
assertThrows(() => {
278+
renameSync(testDir, anotherDir);
279+
}, Error);
280+
} else {
281+
renameSync(testDir, anotherDir);
282+
assertMissing(testDir);
283+
const anotherDirStat = statSync(anotherDir);
284+
assert(anotherDirStat.isDirectory());
285+
}
286+
287+
rmSync(tempDirPath, { recursive: true, force: true });
288+
});
289+
290+
Deno.test("renameSync() throws with Error on *nix and succeeds on Windows when an existing directory is renamed with an existing regular file path", () => {
291+
const tempDirPath = mkdtempSync(resolve(tmpdir(), "renameSync_"));
292+
const testFile = join(tempDirPath, "testFile.txt");
293+
const testDir = join(tempDirPath, "testDir");
294+
295+
const testFd = openSync(testFile, "w");
296+
closeSync(testFd);
297+
mkdirSync(testDir);
298+
299+
if (platform() === "win32") {
300+
renameSync(testDir, testFile);
301+
const fileStat = statSync(testFile);
302+
assert(fileStat.isDirectory());
303+
} else {
304+
assertThrows(() => {
305+
renameSync(testDir, testFile);
306+
}, Error);
307+
}
308+
309+
rmSync(tempDirPath, { recursive: true, force: true });
310+
});
311+
312+
Deno.test({
313+
name:
314+
"renameSync() throws with Error when renaming an existing directory with a valid symlink'd regular file path",
315+
ignore: platform() === "win32",
316+
fn: () => {
317+
const tempDirPath = mkdtempSync(resolve(tmpdir(), "renameSync_"));
318+
const testDir = join(tempDirPath, "testDir");
319+
const testFile = join(tempDirPath, "testFile.txt");
320+
const symlinkFile = join(tempDirPath, "testFile.txt.link");
321+
322+
mkdirSync(testDir);
323+
const testFd = openSync(testFile, "w");
324+
closeSync(testFd);
325+
symlinkSync(testFile, symlinkFile);
326+
327+
assertThrows(() => {
328+
renameSync(testDir, symlinkFile);
329+
}, Error);
330+
331+
rmSync(tempDirPath, { recursive: true, force: true });
332+
},
333+
});
334+
335+
Deno.test({
336+
name:
337+
"renameSync() throws with Error when renaming an existing directory with a valid symlink'd directory path",
338+
ignore: platform() === "win32",
339+
fn: () => {
340+
const tempDirPath = mkdtempSync(resolve(tmpdir(), "renameSync_"));
341+
const testDir = join(tempDirPath, "testDir");
342+
const anotherDir = join(tempDirPath, "anotherDir");
343+
const symlinkDir = join(tempDirPath, "symlinkDir");
344+
345+
mkdirSync(testDir);
346+
mkdirSync(anotherDir);
347+
symlinkSync(anotherDir, symlinkDir);
348+
349+
assertThrows(() => {
350+
renameSync(testDir, symlinkDir);
351+
}, Error);
352+
353+
rmSync(tempDirPath, { recursive: true, force: true });
354+
},
355+
});
356+
357+
Deno.test({
358+
name:
359+
"rename() rejects with Error when renaming an existing directory with a symlink'd file pointing to a non-existent file path",
360+
ignore: platform() === "win32",
361+
fn: () => {
362+
const tempDirPath = mkdtempSync(resolve(tmpdir(), "renameSync_"));
363+
const testDir = join(tempDirPath, "testDir");
364+
const symlinkPath = join(tempDirPath, "symlinkPath");
365+
366+
mkdirSync(testDir);
367+
symlinkSync("non-existent", symlinkPath);
368+
369+
assertThrows(() => {
370+
renameSync(testDir, symlinkPath);
371+
}, Error);
372+
373+
rmSync(tempDirPath, { recursive: true, force: true });
374+
},
375+
});
376+
377+
Deno.test("renameSync() throws with NotFound for renaming a non-existent file", () => {
378+
assertThrows(() => {
379+
renameSync("non-existent-file.txt", "new-name.txt");
380+
}, NotFound);
381+
});

0 commit comments

Comments
 (0)