Skip to content

Commit

Permalink
feat(fs/unstable): add symlink and symlinkSync (#6352)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbronder authored Jan 21, 2025
1 parent 5d41054 commit a2a6305
Show file tree
Hide file tree
Showing 5 changed files with 209 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 @@ -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,
);
} catch (error) {
throw mapError(error);
}
}
}

/**
* 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);
}
}
}
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";
}

0 comments on commit a2a6305

Please sign in to comment.