Skip to content

Commit d499310

Browse files
authored
feat(fs/unstable): add readLink and readLinkSync (#6373)
1 parent ace6a0e commit d499310

File tree

4 files changed

+156
-0
lines changed

4 files changed

+156
-0
lines changed

_tools/node_test_runner/run_test.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import "../../collections/without_all_test.ts";
5151
import "../../collections/zip_test.ts";
5252
import "../../fs/unstable_link_test.ts";
5353
import "../../fs/unstable_read_dir_test.ts";
54+
import "../../fs/unstable_read_link_test.ts";
5455
import "../../fs/unstable_real_path_test.ts";
5556
import "../../fs/unstable_stat_test.ts";
5657
import "../../fs/unstable_symlink_test.ts";

fs/deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"./unstable-link": "./unstable_link.ts",
1818
"./unstable-lstat": "./unstable_lstat.ts",
1919
"./unstable-read-dir": "./unstable_read_dir.ts",
20+
"./unstable-read-link": "./unstable_read_link.ts",
2021
"./unstable-real-path": "./unstable_real_path.ts",
2122
"./unstable-stat": "./unstable_stat.ts",
2223
"./unstable-symlink": "./unstable_symlink.ts",

fs/unstable_read_link.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
3+
import { getNodeFs, isDeno } from "./_utils.ts";
4+
import { mapError } from "./_map_error.ts";
5+
6+
/**
7+
* Resolves to the path destination of the named symbolic link.
8+
*
9+
* Throws Error if called with a hard link.
10+
*
11+
* Requires `allow-read` permission.
12+
*
13+
* @example Usage
14+
* ```ts ignore
15+
* import { readLink } from "@std/fs/unstable-read-link";
16+
* import { symlink } from "@std/fs/unstable-symlink";
17+
* await symlink("./test.txt", "./test_link.txt");
18+
* const target = await readLink("./test_link.txt"); // full path of ./test.txt
19+
* ```
20+
*
21+
* @tags allow-read
22+
*
23+
* @param path The path of the symbolic link.
24+
* @returns A promise that resolves to the file path pointed by the symbolic
25+
* link.
26+
*/
27+
export async function readLink(path: string | URL): Promise<string> {
28+
if (isDeno) {
29+
return Deno.readLink(path);
30+
} else {
31+
try {
32+
return await getNodeFs().promises.readlink(path);
33+
} catch (error) {
34+
throw mapError(error);
35+
}
36+
}
37+
}
38+
39+
/**
40+
* Synchronously returns the path destination of the named symbolic link.
41+
*
42+
* Throws Error if called with a hard link.
43+
*
44+
* Requires `allow-read` permission.
45+
*
46+
* @example Usage
47+
* ```ts ignore
48+
* import { readLinkSync } from "@std/fs/unstable-read-link";
49+
* import { symlinkSync } from "@std/fs/unstable-symlink";
50+
* symlinkSync("./test.txt", "./test_link.txt");
51+
* const target = readLinkSync("./test_link.txt"); // full path of ./test.txt
52+
* ```
53+
*
54+
* @tags allow-read
55+
*
56+
* @param path The path of the symbolic link.
57+
* @returns The file path pointed by the symbolic link.
58+
*/
59+
export function readLinkSync(path: string | URL): string {
60+
if (isDeno) {
61+
return Deno.readLinkSync(path);
62+
} else {
63+
try {
64+
return getNodeFs().readlinkSync(path);
65+
} catch (error) {
66+
throw mapError(error);
67+
}
68+
}
69+
}

fs/unstable_read_link_test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
3+
import { assertEquals, assertRejects, assertThrows } from "@std/assert";
4+
import { readLink, readLinkSync } from "./unstable_read_link.ts";
5+
import { NotFound } from "./unstable_errors.js";
6+
import {
7+
linkSync,
8+
mkdtempSync,
9+
rmSync,
10+
symlinkSync,
11+
writeFileSync,
12+
} from "node:fs";
13+
import { link, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
14+
import { tmpdir } from "node:os";
15+
import { join, resolve } from "node:path";
16+
17+
Deno.test("readLink() can read through symlink", async () => {
18+
const tempDirPath = await mkdtemp(resolve(tmpdir(), "readLink_"));
19+
const testFile = join(tempDirPath, "testFile.txt");
20+
const symlinkFile = join(tempDirPath, "testFile.txt.link");
21+
22+
await writeFile(testFile, "Hello, Standard Library");
23+
await symlink(testFile, symlinkFile);
24+
25+
const realFile = await readLink(symlinkFile);
26+
assertEquals(testFile, realFile);
27+
28+
await rm(tempDirPath, { recursive: true, force: true });
29+
});
30+
31+
Deno.test("readLink() rejects with Error when reading from a hard link", async () => {
32+
const tempDirPath = await mkdtemp(resolve(tmpdir(), "readLink_"));
33+
const testFile = join(tempDirPath, "testFile.txt");
34+
const linkFile = join(tempDirPath, "testFile.txt.hlink");
35+
36+
await writeFile(testFile, "Hello, Standard Library");
37+
await link(testFile, linkFile);
38+
39+
await assertRejects(async () => {
40+
await readLink(linkFile);
41+
}, Error);
42+
43+
await rm(tempDirPath, { recursive: true, force: true });
44+
});
45+
46+
Deno.test("readLink() rejects with NotFound when reading through a non-existent file", async () => {
47+
await assertRejects(async () => {
48+
await readLink("non-existent-file.txt.link");
49+
}, NotFound);
50+
});
51+
52+
Deno.test("readLinkSync() can read through symlink", () => {
53+
const tempDirPath = mkdtempSync(resolve(tmpdir(), "readLink_"));
54+
const testFile = join(tempDirPath, "testFile.txt");
55+
const symlinkFile = join(tempDirPath, "testFile.txt.link");
56+
57+
writeFileSync(testFile, "Hello, Standard Library");
58+
symlinkSync(testFile, symlinkFile);
59+
60+
const realFile = readLinkSync(symlinkFile);
61+
assertEquals(testFile, realFile);
62+
63+
rmSync(tempDirPath, { recursive: true, force: true });
64+
});
65+
66+
Deno.test("readLinkSync() throws Error when reading from a hard link", () => {
67+
const tempDirPath = mkdtempSync(resolve(tmpdir(), "readLinkSync_"));
68+
const testFile = join(tempDirPath, "testFile.txt");
69+
const linkFile = join(tempDirPath, "testFile.txt.hlink");
70+
71+
writeFileSync(testFile, "Hello, Standard Library!");
72+
linkSync(testFile, linkFile);
73+
74+
assertThrows(() => {
75+
readLinkSync(linkFile);
76+
}, Error);
77+
78+
rmSync(tempDirPath, { recursive: true, force: true });
79+
});
80+
81+
Deno.test("readLinkSync() throws NotFound when reading through a non-existent file", () => {
82+
assertThrows(() => {
83+
readLinkSync("non-existent-file.txt.hlink");
84+
}, NotFound);
85+
});

0 commit comments

Comments
 (0)