From f1bcafc5840f73716a668ef99b04fcbed494817d Mon Sep 17 00:00:00 2001
From: Yun Lai <lyonlai1984@gmail.com>
Date: Tue, 26 Jul 2022 18:19:20 +1000
Subject: [PATCH] feat(fs): add localPathIsSymbolicLink and readLocalSymlink
 (#16673)

* fix: add localPathIsSymbolicLink and readLocalSymlink to fs module

* Update lib/util/fs/index.ts

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>

* Update lib/util/fs/index.ts

* Apply suggestions from code review

Co-authored-by: Sergei Zharinov <zharinov@users.noreply.github.com>

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
Co-authored-by: Rhys Arkins <rhys@arkins.net>
Co-authored-by: Sergei Zharinov <zharinov@users.noreply.github.com>
---
 lib/util/fs/index.spec.ts | 57 +++++++++++++++++++++++++++++++++++++++
 lib/util/fs/index.ts      | 25 +++++++++++++++++
 2 files changed, 82 insertions(+)

diff --git a/lib/util/fs/index.spec.ts b/lib/util/fs/index.spec.ts
index 0f221e33e1..6a257f0127 100644
--- a/lib/util/fs/index.spec.ts
+++ b/lib/util/fs/index.spec.ts
@@ -18,11 +18,13 @@ import {
   listCacheDir,
   localPathExists,
   localPathIsFile,
+  localPathIsSymbolicLink,
   outputCacheFile,
   privateCacheDir,
   readCacheFile,
   readLocalDirectory,
   readLocalFile,
+  readLocalSymlink,
   readSystemFile,
   renameLocalFile,
   rmCache,
@@ -204,6 +206,32 @@ describe('util/fs/index', () => {
     });
   });
 
+  describe('readLocalSymlink', () => {
+    it('reads symlink', async () => {
+      await writeLocalFile('test/test.txt', '');
+      await fs.symlink(
+        join(localDir, 'test/test.txt'),
+        join(localDir, 'test/test')
+      );
+
+      const result = await readLocalSymlink('test/test');
+
+      expect(result).not.toBeNull();
+    });
+
+    it('return null when link not exists', async () => {
+      await writeLocalFile('test/test.txt', '');
+      await fs.symlink(
+        join(localDir, 'test/test.txt'),
+        join(localDir, 'test/test')
+      );
+
+      const notExistsResult = await readLocalSymlink('test/not-exists');
+
+      expect(notExistsResult).toBeNull();
+    });
+  });
+
   describe('findLocalSiblingOrParent', () => {
     it('returns path for file', async () => {
       await writeLocalFile('crates/one/Cargo.toml', 'foo');
@@ -301,6 +329,35 @@ describe('util/fs/index', () => {
     });
   });
 
+  describe('localPathIsSymbolicLink', () => {
+    it('returns false for file', async () => {
+      const path = `${localDir}/file.txt`;
+      await fs.outputFile(path, 'foobar');
+      expect(await localPathIsSymbolicLink(path)).toBeFalse();
+    });
+
+    it('returns false for directory', async () => {
+      const path = `${localDir}/foobar`;
+      await fs.mkdir(path);
+      expect(await localPathIsSymbolicLink(path)).toBeFalse();
+    });
+
+    it('returns false for non-existing path', async () => {
+      const path = `${localDir}/file.txt`;
+      expect(await localPathIsSymbolicLink(path)).toBeFalse();
+    });
+
+    it('returns true for symlink', async () => {
+      const source = `${localDir}/test/test.txt`;
+      const target = `${localDir}/test/test`;
+      await fs.outputFile(source, 'foobar');
+      await fs.symlink(source, target);
+
+      const result = await localPathIsSymbolicLink('test/test');
+      expect(result).toBeTrue();
+    });
+  });
+
   describe('findUpLocal', () => {
     beforeEach(() => {
       GlobalConfig.set({ localDir: '/abs/path/to/local/dir' });
diff --git a/lib/util/fs/index.ts b/lib/util/fs/index.ts
index c3cb9a7419..6f65fecc80 100644
--- a/lib/util/fs/index.ts
+++ b/lib/util/fs/index.ts
@@ -43,6 +43,19 @@ export async function readLocalFile(
   }
 }
 
+export async function readLocalSymlink(
+  fileName: string
+): Promise<string | null> {
+  const localFileName = ensureLocalPath(fileName);
+  try {
+    const linkContent = await fs.readlink(localFileName);
+    return linkContent;
+  } catch (err) {
+    logger.trace({ err }, 'Error reading local symlink');
+    return null;
+  }
+}
+
 export async function writeLocalFile(
   fileName: string,
   fileContent: string | Buffer
@@ -160,6 +173,18 @@ export async function localPathIsFile(pathName: string): Promise<boolean> {
   }
 }
 
+export async function localPathIsSymbolicLink(
+  pathName: string
+): Promise<boolean> {
+  const path = ensureLocalPath(pathName);
+  try {
+    const s = await fs.lstat(path);
+    return s.isSymbolicLink();
+  } catch (_) {
+    return false;
+  }
+}
+
 /**
  * Find a file or directory by walking up parent directories within localDir
  */
-- 
GitLab