Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fs/unstable): add symlink and symlinkSync #6352

Merged
merged 4 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -51,6 +51,7 @@ import "../../collections/without_all_test.ts";
import "../../collections/zip_test.ts";
import "../../fs/unstable_read_dir_test.ts";
import "../../fs/unstable_stat_test.ts";
import "../../fs/unstable_symlink_test.ts";
import "../../fs/unstable_lstat_test.ts";
import "../../fs/unstable_chmod_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 @@ -17,6 +17,7 @@
"./unstable-lstat": "./unstable_lstat.ts",
"./unstable-read-dir": "./unstable_read_dir.ts",
"./unstable-stat": "./unstable_stat.ts",
"./unstable-symlink": "./unstable_symlink.ts",
"./unstable-types": "./unstable_types.ts",
"./walk": "./walk.ts"
}
Expand Down
79 changes: 79 additions & 0 deletions fs/unstable_symlink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright 2018-2025 the Deno authors. MIT license.

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

/**
* Creates `newpath` as a symbolic link to `oldpath`.
*
* The `options.type` parameter can be set to `"file"`, `"dir"` or `"junction"`.
* This argument is only available on Windows and ignored on other platforms.
*
* Requires full `allow-read` and `allow-write` permissions.
*
* @example Usage
* ```ts ignore
* import { symlink } from "@std/fs/unstable-symlink";
* await symlink("README.md", "README.md.link");
* ```
*
* @tags allow-read, allow-write
*
* @param oldpath The path of the resource pointed by the symbolic link.
* @param newpath The path of the symbolic link.
*/
export async function symlink(
oldpath: string | URL,
newpath: string | URL,
options?: SymlinkOptions,
): Promise<void> {
if (isDeno) {
return Deno.symlink(oldpath, newpath, options);
} else {
try {
return await getNodeFs().promises.symlink(
oldpath,
newpath,
options?.type,

Check warning on line 38 in fs/unstable_symlink.ts

View check run for this annotation

Codecov / codecov/patch

fs/unstable_symlink.ts#L34-L38

Added lines #L34 - L38 were not covered by tests
);
} catch (error) {
throw mapError(error);
}
}

Check warning on line 43 in fs/unstable_symlink.ts

View check run for this annotation

Codecov / codecov/patch

fs/unstable_symlink.ts#L40-L43

Added lines #L40 - L43 were not covered by tests
}

/**
* Creates `newpath` as a symbolic link to `oldpath`.
*
* The `options.type` parameter can be set to `"file"`, `"dir"` or `"junction"`.
* This argument is only available on Windows and ignored on other platforms.
*
* Requires full `allow-read` and `allow-write` permissions.
*
* @example Usage
* ```ts ignore
* import { symlinkSync } from "@std/fs/unstable-symlink";
* symlinkSync("README.md", "README.md.link");
* ```
*
* @tags allow-read, allow-write
*
* @param oldpath The path of the resource pointed by the symbolic link.
* @param newpath The path of the symbolic link.
*/
export function symlinkSync(
oldpath: string | URL,
newpath: string | URL,
options?: SymlinkOptions,
): void {
if (isDeno) {
return Deno.symlinkSync(oldpath, newpath, options);
} else {
try {
return getNodeFs().symlinkSync(oldpath, newpath, options?.type);
} catch (error) {
throw mapError(error);
}
}

Check warning on line 78 in fs/unstable_symlink.ts

View check run for this annotation

Codecov / codecov/patch

fs/unstable_symlink.ts#L73-L78

Added lines #L73 - L78 were not covered by tests
}
118 changes: 118 additions & 0 deletions fs/unstable_symlink_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright 2018-2025 the Deno authors. MIT license.

import { assert, assertRejects, assertThrows } from "@std/assert";
import { symlink, symlinkSync } from "./unstable_symlink.ts";
import { AlreadyExists } from "./unstable_errors.js";
import { lstat, mkdir, mkdtemp, open, rm, stat } from "node:fs/promises";
import {
closeSync,
lstatSync,
mkdirSync,
mkdtempSync,
openSync,
rmSync,
statSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const moduleDir = dirname(fileURLToPath(import.meta.url));
const testdataDir = resolve(moduleDir, "testdata");

Deno.test("symlink() creates a link to a regular file", async () => {
const tempDirPath = await mkdtemp(resolve(tmpdir(), "symlink_"));
const testFile = join(tempDirPath, "testFile.txt");
const symlinkPath = join(tempDirPath, "testFile.txt.link");

const tempFh = await open(testFile, "w");
await symlink(testFile, symlinkPath);

const symlinkLstat = await lstat(symlinkPath);
const fileStat = await stat(testFile);

assert(symlinkLstat.isSymbolicLink);
assert(fileStat.isFile);

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

Deno.test("symlink() creates a link to a directory", async () => {
const tempDirPath = await mkdtemp(resolve(tmpdir(), "symlink_"));
const testDir = join(tempDirPath, "testDir");
const symlinkPath = join(tempDirPath, "testDir.link");

await mkdir(testDir);
await symlink(testDir, symlinkPath);

const symlinkLstat = await lstat(symlinkPath);
const dirStat = await stat(testDir);

assert(symlinkLstat.isSymbolicLink);
assert(dirStat.isDirectory);

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

Deno.test(
"symlink() rejects with AlreadyExists for creating the same link path to the same file path",
async () => {
const existingFile = join(testdataDir, "0.ts");
const existingSymlink = join(testdataDir, "0-link");

await assertRejects(async () => {
await symlink(existingFile, existingSymlink);
}, AlreadyExists);
},
);

Deno.test(
"symlinkSync() creates a link to a regular file",
() => {
const tempDirPath = mkdtempSync(resolve(tmpdir(), "symlinkSync_"));
const filePath = join(tempDirPath, "testFile.txt");
const symlinkPath = join(tempDirPath, "testFile.txt.link");

const tempFd = openSync(filePath, "w");
symlinkSync(filePath, symlinkPath);

const symlinkLstat = lstatSync(symlinkPath);
const fileStat = statSync(filePath);

assert(symlinkLstat.isSymbolicLink);
assert(fileStat.isFile);

closeSync(tempFd);
rmSync(tempDirPath, { recursive: true, force: true });
},
);

Deno.test("symlinkSync() creates a link to a directory", () => {
const tempDirPath = mkdtempSync(resolve(tmpdir(), "symlinkSync_"));
const testDir = join(tempDirPath, "testDir");
const symlinkPath = join(tempDirPath, "testDir.link");

mkdirSync(testDir);
symlinkSync(testDir, symlinkPath);

const symlinkLstat = lstatSync(symlinkPath);
const dirStat = statSync(testDir);

assert(symlinkLstat.isSymbolicLink);
assert(dirStat.isDirectory);

rmSync(tempDirPath, { recursive: true, force: true });
});

Deno.test(
"symlinkSync() throws with AlreadyExists for creating the same link path to the same file path",
() => {
const existingFile = join(testdataDir, "0.ts");
const existingSymlink = join(testdataDir, "0-link");

assertThrows(() => {
symlinkSync(existingFile, existingSymlink);
}, AlreadyExists);
},
);
10 changes: 10 additions & 0 deletions fs/unstable_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,13 @@ export interface DirEntry {
* `FileInfo.isFile` and `FileInfo.isDirectory`. */
isSymlink: boolean;
}

/**
* Options that can be used with {@linkcode symlink} and
* {@linkcode symlinkSync}.
*/
export interface SymlinkOptions {
/** Specify the symbolic link type as file, directory or NTFS junction. This
* option only applies to Windows and is ignored on other operating systems. */
type: "file" | "dir" | "junction";
}
Loading