From 242db3a089cded28a4c12b9e211371944183ace4 Mon Sep 17 00:00:00 2001 From: William Hilton Date: Sun, 17 Mar 2019 21:44:01 -0400 Subject: [PATCH] feat: backport symlink support --- src/CacheFS.js | 55 +++++++++++++- src/__tests__/fs.spec.js | 155 +++++++++++++++++++++++++++++++++++++++ src/index.js | 42 ++++++++++- 3 files changed, 248 insertions(+), 4 deletions(-) diff --git a/src/CacheFS.js b/src/CacheFS.js index 6e7b336..61fa41e 100755 --- a/src/CacheFS.js +++ b/src/CacheFS.js @@ -98,11 +98,28 @@ module.exports = class CacheFS { } return _root; } - _lookup(filepath) { + _lookup(filepath, follow = true) { let dir = this._root; + let partialPath = '/' for (let part of path.split(filepath)) { dir = dir.get(part); if (!dir) throw new ENOENT(filepath); + // Follow symlinks + if (follow) { + const stat = dir.get(STAT) + if (stat.type === 'symlink') { + let target = stat.target + if (!target.startsWith('/')) { + target = path.normalize(path.join(partialPath, target)) + } + dir = this._lookup(target) + } + if (!partialPath) { + partialPath = part + } else { + partialPath = path.join(partialPath, part) + } + } } return dir; } @@ -184,4 +201,40 @@ module.exports = class CacheFS { stat(filepath) { return this._lookup(filepath).get(STAT); } + lstat(filepath) { + return this._lookup(filepath, false).get(STAT); + } + readlink(filepath) { + return this._lookup(filepath, false).get(STAT).target; + } + symlink(target, filepath) { + let ino, mode; + try { + let oldStat = this.stat(filepath); + if (mode === null) { + mode = oldStat.mode; + } + ino = oldStat.ino; + } catch (err) {} + if (mode == null) { + mode = 0o666; + } + if (ino == null) { + ino = this.autoinc(); + } + let dir = this._lookup(path.dirname(filepath)); + let basename = path.basename(filepath); + let stat = { + mode, + type: "symlink", + target, + size: 0, + mtimeMs: Date.now(), + ino, + }; + let entry = new Map(); + entry.set(STAT, stat); + dir.set(basename, entry); + return stat; + } }; diff --git a/src/__tests__/fs.spec.js b/src/__tests__/fs.spec.js index e6a013f..3a400ec 100755 --- a/src/__tests__/fs.spec.js +++ b/src/__tests__/fs.spec.js @@ -270,4 +270,159 @@ describe("fs module", () => { }); }); }); + + describe("symlink", () => { + it("symlink a file and read/write to it", done => { + fs.mkdir("/symlink", () => { + fs.writeFile("/symlink/a.txt", "hello", () => { + fs.symlink("/symlink/a.txt", "/symlink/b.txt", () => { + fs.readFile("/symlink/b.txt", "utf8", (err, data) => { + expect(err).toBe(null) + expect(data).toBe("hello") + fs.writeFile("/symlink/b.txt", "world", () => { + fs.readFile("/symlink/a.txt", "utf8", (err, data) => { + expect(err).toBe(null) + expect(data).toBe("world"); + done(); + }) + }) + }); + }); + }); + }); + }); + it("symlink a file and read/write to it (relative)", done => { + fs.mkdir("/symlink", () => { + fs.writeFile("/symlink/a.txt", "hello", () => { + fs.symlink("a.txt", "/symlink/b.txt", () => { + fs.readFile("/symlink/b.txt", "utf8", (err, data) => { + expect(err).toBe(null) + expect(data).toBe("hello") + fs.writeFile("/symlink/b.txt", "world", () => { + fs.readFile("/symlink/a.txt", "utf8", (err, data) => { + expect(err).toBe(null) + expect(data).toBe("world"); + done(); + }) + }) + }); + }); + }); + }); + }); + it("symlink a directory and read/write to it", done => { + fs.mkdir("/symlink", () => { + fs.mkdir("/symlink/a", () => { + fs.writeFile("/symlink/a/file.txt", "data", () => { + fs.symlink("/symlink/a", "/symlink/b", () => { + fs.readdir("/symlink/b", (err, data) => { + expect(err).toBe(null) + expect(data.includes("file.txt")).toBe(true); + fs.readFile("/symlink/b/file.txt", "utf8", (err, data) => { + expect(err).toBe(null) + expect(data).toBe("data") + fs.writeFile("/symlink/b/file2.txt", "world", () => { + fs.readFile("/symlink/a/file2.txt", "utf8", (err, data) => { + expect(err).toBe(null); + expect(data).toBe("world"); + done(); + }) + }) + }); + }); + }); + }); + }); + }); + }); + it("symlink a directory and read/write to it (relative)", done => { + fs.mkdir("/symlink", () => { + fs.mkdir("/symlink/a", () => { + fs.mkdir("/symlink/b", () => { + fs.writeFile("/symlink/a/file.txt", "data", () => { + fs.symlink("../a", "/symlink/b/c", () => { + fs.readdir("/symlink/b/c", (err, data) => { + expect(err).toBe(null) + expect(data.includes("file.txt")).toBe(true); + fs.readFile("/symlink/b/c/file.txt", "utf8", (err, data) => { + expect(err).toBe(null) + expect(data).toBe("data") + fs.writeFile("/symlink/b/c/file2.txt", "world", () => { + fs.readFile("/symlink/a/file2.txt", "utf8", (err, data) => { + expect(err).toBe(null); + expect(data).toBe("world"); + done(); + }) + }) + }); + }); + }); + }); + }); + }); + }); + }); + it("unlink doesn't follow symlinks", done => { + fs.mkdir("/symlink", () => { + fs.mkdir("/symlink/del", () => { + fs.writeFile("/symlink/del/file.txt", "data", () => { + fs.symlink("/symlink/del/file.txt", "/symlink/del/file2.txt", () => { + fs.readdir("/symlink/del", (err, data) => { + expect(err).toBe(null) + expect(data.includes("file.txt")).toBe(true) + expect(data.includes("file2.txt")).toBe(true) + fs.unlink("/symlink/del/file2.txt", (err, data) => { + expect(err).toBe(null) + fs.readdir("/symlink/del", (err, data) => { + expect(err).toBe(null) + expect(data.includes("file.txt")).toBe(true) + expect(data.includes("file2.txt")).toBe(false) + done(); + }); + }); + }); + }); + }); + }); + }); + }); + it("lstat doesn't follow symlinks", done => { + fs.mkdir("/symlink", () => { + fs.mkdir("/symlink/lstat", () => { + fs.writeFile("/symlink/lstat/file.txt", "data", () => { + fs.symlink("/symlink/lstat/file.txt", "/symlink/lstat/file2.txt", () => { + fs.stat("/symlink/lstat/file2.txt", (err, stat) => { + expect(err).toBe(null) + expect(stat.isFile()).toBe(true) + expect(stat.isSymbolicLink()).toBe(false) + fs.lstat("/symlink/lstat/file2.txt", (err, stat) => { + expect(err).toBe(null) + expect(stat.isFile()).toBe(false) + expect(stat.isSymbolicLink()).toBe(true) + done(); + }); + }); + }); + }); + }); + }); + }); + }); + + describe("readlink", () => { + it("readlink returns the target path", done => { + fs.mkdir("/readlink", () => { + fs.writeFile("/readlink/a.txt", "hello", () => { + fs.symlink("/readlink/a.txt", "/readlink/b.txt", () => { + fs.readlink("/readlink/b.txt", "utf8", (err, data) => { + expect(err).toBe(null) + expect(data).toBe("/readlink/a.txt") + done(); + }); + }); + }); + }); + }); + }); + }); diff --git a/src/index.js b/src/index.js index 76e575d..a4abbaa 100755 --- a/src/index.js +++ b/src/index.js @@ -35,6 +35,8 @@ module.exports = class FS { this.rename = this.rename.bind(this) this.stat = this.stat.bind(this) this.lstat = this.lstat.bind(this) + this.readlink = this.readlink.bind(this) + this.symlink = this.symlink.bind(this) } _cleanParams(filepath, opts, cb, stopClock = null, save = false) { filepath = path.normalize(filepath); @@ -242,8 +244,42 @@ module.exports = class FS { .catch(cb); } lstat(filepath, opts, cb) { - return this.stat(filepath, opts, cb); + [filepath, opts, cb] = this._cleanParams(filepath, opts, cb); + this.superblockPromise + .then(() => { + try { + let data = this._cache.lstat(filepath); + return cb(null, new Stat(data)); + } catch (err) { + return cb(err); + } + }) + .catch(cb); + } + readlink(filepath, opts, cb) { + [filepath, opts, cb] = this._cleanParams(filepath, opts, cb); + this.superblockPromise + .then(() => { + try { + let data = this._cache.readlink(filepath); + return cb(null, data); + } catch (err) { + return cb(err); + } + }) + .catch(cb); + } + symlink(target, filepath, cb) { + [target, filepath, cb] = this._cleanParams2(target, filepath, cb, null, true); + this.superblockPromise + .then(() => { + try { + this._cache.symlink(target, filepath); + return cb(null); + } catch (err) { + return cb(err); + } + }) + .catch(cb); } - readlink() {} - symlink() {} }