diff --git a/fs/ensure_symlink.ts b/fs/ensure_symlink.ts index 1345718aa..bc2b0e5ad 100644 --- a/fs/ensure_symlink.ts +++ b/fs/ensure_symlink.ts @@ -19,6 +19,8 @@ function resolveSymlinkTarget(target: string | URL, linkName: string | URL) { /** * Ensures that the link exists, and points to a valid file. * If the directory structure does not exist, it is created. + * If the link already exists, it is not modified but error is thrown if it is not point to the given target. + * Requires the `--allow-read` and `--allow-write` flag. * * @param target the source file path * @param linkName the destination link path @@ -45,12 +47,28 @@ export async function ensureSymlink( if (!(error instanceof Deno.errors.AlreadyExists)) { throw error; } + const linkStatInfo = await Deno.lstat(linkName); + if (!linkStatInfo.isSymlink) { + const type = getFileInfoType(linkStatInfo); + throw new Deno.errors.AlreadyExists( + `A '${type}' already exists at the path: ${linkName}`, + ); + } + const linkPath = await Deno.readLink(linkName); + const linkRealPath = resolve(linkPath); + if (linkRealPath !== targetRealPath) { + throw new Deno.errors.AlreadyExists( + `A symlink targeting to an undesired path already exists: ${linkName} -> ${linkRealPath}`, + ); + } } } /** * Ensures that the link exists, and points to a valid file. * If the directory structure does not exist, it is created. + * If the link already exists, it is not modified but error is thrown if it is not point to the given target. + * Requires the `--allow-read` and `--allow-write` flag. * * @param target the source file path * @param linkName the destination link path @@ -77,5 +95,19 @@ export function ensureSymlinkSync( if (!(error instanceof Deno.errors.AlreadyExists)) { throw error; } + const linkStatInfo = Deno.lstatSync(linkName); + if (!linkStatInfo.isSymlink) { + const type = getFileInfoType(linkStatInfo); + throw new Deno.errors.AlreadyExists( + `A '${type}' already exists at the path: ${linkName}`, + ); + } + const linkPath = Deno.readLinkSync(linkName); + const linkRealPath = resolve(linkPath); + if (linkRealPath !== targetRealPath) { + throw new Deno.errors.AlreadyExists( + `A symlink targeting to an undesired path already exists: ${linkName} -> ${linkRealPath}`, + ); + } } } diff --git a/fs/ensure_symlink_test.ts b/fs/ensure_symlink_test.ts index e9f98f743..eb5cf959a 100644 --- a/fs/ensure_symlink_test.ts +++ b/fs/ensure_symlink_test.ts @@ -80,6 +80,74 @@ Deno.test("ensureSymlinkSync() ensures linkName links to target", function () { Deno.removeSync(testDir, { recursive: true }); }); +Deno.test("ensureSymlink() rejects if the linkName path already exist", async function () { + const testDir = path.join(testdataDir, "link_file_5"); + const linkFile = path.join(testDir, "test.txt"); + const linkDir = path.join(testDir, "test_dir"); + const linkSymlink = path.join(testDir, "test_symlink"); + const targetFile = path.join(testDir, "target.txt"); + + await Deno.mkdir(testDir, { recursive: true }); + await Deno.writeTextFile(linkFile, "linkFile"); + await Deno.mkdir(linkDir); + await Deno.symlink("non-existent", linkSymlink, { type: "file" }); + await Deno.writeTextFile(targetFile, "targetFile"); + + await assertRejects( + async () => { + await ensureSymlink(targetFile, linkFile); + }, + ); + await assertRejects( + async () => { + await ensureSymlink(targetFile, linkDir); + }, + ); + await assertRejects( + async () => { + await ensureSymlink(targetFile, linkSymlink); + }, + ); + + assertEquals(await Deno.readTextFile(linkFile), "linkFile"); + assertEquals((await Deno.stat(linkDir)).isDirectory, true); + assertEquals(await Deno.readLink(linkSymlink), "non-existent"); + assertEquals(await Deno.readTextFile(targetFile), "targetFile"); + + await Deno.remove(testDir, { recursive: true }); +}); + +Deno.test("ensureSymlinkSync() throws if the linkName path already exist", function () { + const testDir = path.join(testdataDir, "link_file_6"); + const linkFile = path.join(testDir, "test.txt"); + const linkDir = path.join(testDir, "test_dir"); + const linkSymlink = path.join(testDir, "test_symlink"); + const targetFile = path.join(testDir, "target.txt"); + + Deno.mkdirSync(testDir, { recursive: true }); + Deno.writeTextFileSync(linkFile, "linkFile"); + Deno.mkdirSync(linkDir); + Deno.symlinkSync("non-existent", linkSymlink, { type: "file" }); + Deno.writeTextFileSync(targetFile, "targetFile"); + + assertThrows(() => { + ensureSymlinkSync(targetFile, linkFile); + }); + assertThrows(() => { + ensureSymlinkSync(targetFile, linkDir); + }); + assertThrows(() => { + ensureSymlinkSync(targetFile, linkSymlink); + }); + + assertEquals(Deno.readTextFileSync(linkFile), "linkFile"); + assertEquals(Deno.statSync(linkDir).isDirectory, true); + assertEquals(Deno.readLinkSync(linkSymlink), "non-existent"); + assertEquals(Deno.readTextFileSync(targetFile), "targetFile"); + + Deno.removeSync(testDir, { recursive: true }); +}); + Deno.test("ensureSymlink() ensures dir linkName links to dir target", async function () { const testDir = path.join(testdataDir, "link_file_origin_3"); const linkDir = path.join(testdataDir, "link_file_link_3");