From 0baf5ed43c75225c639a3d1d366339bd6b8f9f56 Mon Sep 17 00:00:00 2001 From: William Hilton Date: Thu, 14 Mar 2019 22:58:35 -0400 Subject: [PATCH 1/3] feat: add symlink and readlink to CacheFS - TODO: test in fallback fs --- src/CacheFS.js | 45 +++++++++++- src/PromisifiedFS.js | 17 ++++- src/__tests__/fs.promises.spec.js | 98 +++++++++++++++++++++++++++ src/__tests__/fs.spec.js | 109 ++++++++++++++++++++++++++++++ src/index.js | 12 +++- 5 files changed, 274 insertions(+), 7 deletions(-) diff --git a/src/CacheFS.js b/src/CacheFS.js index 082a135..ffe6d0f 100755 --- a/src/CacheFS.js +++ b/src/CacheFS.js @@ -101,11 +101,18 @@ module.exports = class CacheFS { } return _root; } - _lookup(filepath) { + _lookup(filepath, follow = true) { let dir = this._root; 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') { + dir = this._lookup(stat.target) + } + } } return dir; } @@ -187,4 +194,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/PromisifiedFS.js b/src/PromisifiedFS.js index 0b8c7c2..8b81759 100644 --- a/src/PromisifiedFS.js +++ b/src/PromisifiedFS.js @@ -51,6 +51,8 @@ module.exports = class PromisifiedFS { this.rename = this._wrap(this._rename.bind(this)) this.stat = this._wrap(this._stat.bind(this)) this.lstat = this._wrap(this._lstat.bind(this)) + this.readlink = this._wrap(this._readlink.bind(this)) + this.symlink = this._wrap(this._symlink.bind(this)) // Needed so things don't break if you destructure fs and pass individual functions around this.readFile = this.readFile.bind(this) this.writeFile = this.writeFile.bind(this) @@ -174,8 +176,17 @@ module.exports = class PromisifiedFS { return new Stat(data); } async _lstat(filepath, opts) { - return this.stat(filepath, opts); + ;[filepath, opts] = cleanParams(filepath, opts); + let data = this._cache.lstat(filepath); + return new Stat(data); + } + async _readlink(filepath, opts) { + ;[filepath, opts] = cleanParams(filepath, opts); + return this._cache.readlink(filepath); + } + async _symlink(target, filepath) { + ;[target, filepath] = cleanParams2(target, filepath); + this._cache.symlink(target, filepath); + return null; } - readlink() {} - symlink() {} } diff --git a/src/__tests__/fs.promises.spec.js b/src/__tests__/fs.promises.spec.js index ff1abda..e39a10b 100755 --- a/src/__tests__/fs.promises.spec.js +++ b/src/__tests__/fs.promises.spec.js @@ -249,4 +249,102 @@ describe("fs.promises module", () => { }); }); }); + + describe("symlink", () => { + it("symlink a file and read/write to it", done => { + fs.mkdir("/symlink").finally(() => { + fs.writeFile("/symlink/a.txt", "hello").then(() => { + fs.symlink("/symlink/a.txt", "/symlink/b.txt").then(() => { + fs.readFile("/symlink/b.txt", "utf8").then(data => { + expect(data).toBe("hello") + fs.writeFile("/symlink/b.txt", "world").then(() => { + fs.readFile("/symlink/a.txt", "utf8").then(data => { + expect(data).toBe("world"); + done(); + }) + }) + }); + }); + }); + }); + }); + it("symlink a directory and read/write to it", done => { + fs.mkdir("/symlink").finally(() => { + fs.mkdir("/symlink/a").finally(() => { + fs.writeFile("/symlink/a/file.txt", "data").then(() => { + fs.symlink("/symlink/a", "/symlink/b").then(() => { + fs.readdir("/symlink/b").then(data => { + expect(data.includes("file.txt")).toBe(true); + fs.readFile("/symlink/b/file.txt", "utf8").then(data => { + expect(data).toBe("data") + fs.writeFile("/symlink/b/file2.txt", "world").then(() => { + fs.readFile("/symlink/a/file2.txt", "utf8").then(data => { + expect(data).toBe("world"); + done(); + }) + }) + }); + }); + }); + }); + }); + }); + }); + it("unlink doesn't follow symlinks", done => { + fs.mkdir("/symlink").finally(() => { + fs.mkdir("/symlink/del").finally(() => { + fs.writeFile("/symlink/del/file.txt", "data").then(() => { + fs.symlink("/symlink/del/file.txt", "/symlink/del/file2.txt").then(() => { + fs.readdir("/symlink/del").then(data => { + expect(data.includes("file.txt")).toBe(true) + expect(data.includes("file2.txt")).toBe(true) + fs.unlink("/symlink/del/file2.txt").then(data => { + fs.readdir("/symlink/del").then(data => { + 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").finally(() => { + fs.mkdir("/symlink/lstat").finally(() => { + fs.writeFile("/symlink/lstat/file.txt", "data").then(() => { + fs.symlink("/symlink/lstat/file.txt", "/symlink/lstat/file2.txt").then(() => { + fs.stat("/symlink/lstat/file2.txt").then(stat => { + expect(stat.isFile()).toBe(true) + expect(stat.isSymbolicLink()).toBe(false) + fs.lstat("/symlink/lstat/file2.txt").then(stat => { + expect(stat.isFile()).toBe(false) + expect(stat.isSymbolicLink()).toBe(true) + done(); + }); + }); + }); + }); + }); + }); + }); + }); + + describe("readlink", () => { + it("readlink returns the target path", done => { + fs.mkdir("/readlink").finally(() => { + fs.writeFile("/readlink/a.txt", "hello").then(() => { + fs.symlink("/readlink/a.txt", "/readlink/b.txt").then(() => { + fs.readlink("/readlink/b.txt", "utf8").then(data => { + expect(data).toBe("/readlink/a.txt") + done(); + }); + }); + }); + }); + }); + }); + }); diff --git a/src/__tests__/fs.spec.js b/src/__tests__/fs.spec.js index e6a013f..4ed58fa 100755 --- a/src/__tests__/fs.spec.js +++ b/src/__tests__/fs.spec.js @@ -270,4 +270,113 @@ 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 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("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 7e71d2b..c8cb08b 100755 --- a/src/index.js +++ b/src/index.js @@ -64,8 +64,14 @@ module.exports = class FS { } lstat(filepath, opts, cb) { const [resolve, reject] = wrapCallback(opts, cb); - this.promises.stat(filepath).then(resolve).catch(reject); + this.promises.lstat(filepath).then(resolve).catch(reject); + } + readlink(filepath, opts, cb) { + const [resolve, reject] = wrapCallback(opts, cb); + this.promises.readlink(filepath, opts).then(resolve).catch(reject); + } + symlink(target, filepath, opts, cb) { + const [resolve, reject] = wrapCallback(opts, cb); + this.promises.symlink(target, filepath, opts).then(resolve).catch(reject); } - readlink() {} - symlink() {} } From f4610d3b9b534ca7987501b925854041160c1d3e Mon Sep 17 00:00:00 2001 From: William Hilton Date: Thu, 14 Mar 2019 23:24:13 -0400 Subject: [PATCH 2/3] fix: symlinks should handle relative paths --- src/CacheFS.js | 13 ++++++++- src/IdbBackend.js | 2 +- src/__tests__/fs.promises.spec.js | 41 +++++++++++++++++++++++++++ src/__tests__/fs.spec.js | 46 +++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 2 deletions(-) diff --git a/src/CacheFS.js b/src/CacheFS.js index ffe6d0f..3c3434c 100755 --- a/src/CacheFS.js +++ b/src/CacheFS.js @@ -103,6 +103,7 @@ module.exports = class CacheFS { } _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); @@ -110,7 +111,17 @@ module.exports = class CacheFS { if (follow) { const stat = dir.get(STAT) if (stat.type === 'symlink') { - dir = this._lookup(stat.target) + let target = stat.target + if (!target.startsWith('/')) { + target = path.normalize(path.join(partialPath, target)) + console.log('TARGET', target) + } + dir = this._lookup(target) + } + if (!partialPath) { + partialPath = part + } else { + partialPath = path.join(partialPath, part) } } } diff --git a/src/IdbBackend.js b/src/IdbBackend.js index becc0b9..a975cc1 100644 --- a/src/IdbBackend.js +++ b/src/IdbBackend.js @@ -23,7 +23,7 @@ module.exports = class IdbBackend { await idb.set("!root", superblock, this._store); await idb.del("!locked", this._store); this._saving = null - console.log(`${iam} released lock`) + // console.log(`${iam} released lock`) done() }, 500) } diff --git a/src/__tests__/fs.promises.spec.js b/src/__tests__/fs.promises.spec.js index e39a10b..14c464b 100755 --- a/src/__tests__/fs.promises.spec.js +++ b/src/__tests__/fs.promises.spec.js @@ -268,6 +268,23 @@ describe("fs.promises module", () => { }); }); }); + it("symlink a file and read/write to it (relative)", done => { + fs.mkdir("/symlink").finally(() => { + fs.writeFile("/symlink/a.txt", "hello").then(() => { + fs.symlink("a.txt", "/symlink/b.txt").then(() => { + fs.readFile("/symlink/b.txt", "utf8").then(data => { + expect(data).toBe("hello") + fs.writeFile("/symlink/b.txt", "world").then(() => { + fs.readFile("/symlink/a.txt", "utf8").then(data => { + expect(data).toBe("world"); + done(); + }) + }) + }); + }).catch(console.log); + }); + }); + }); it("symlink a directory and read/write to it", done => { fs.mkdir("/symlink").finally(() => { fs.mkdir("/symlink/a").finally(() => { @@ -290,6 +307,30 @@ describe("fs.promises module", () => { }); }); }); + it("symlink a directory and read/write to it (relative)", done => { + fs.mkdir("/symlink").finally(() => { + fs.mkdir("/symlink/a").finally(() => { + fs.mkdir("/symlink/b").finally(() => { + fs.writeFile("/symlink/a/file.txt", "data").then(() => { + fs.symlink("../a", "/symlink/b/c").then(() => { + fs.readdir("/symlink/b/c").then(data => { + expect(data.includes("file.txt")).toBe(true); + fs.readFile("/symlink/b/c/file.txt", "utf8").then(data => { + expect(data).toBe("data") + fs.writeFile("/symlink/b/c/file2.txt", "world").then(() => { + fs.readFile("/symlink/a/file2.txt", "utf8").then(data => { + expect(data).toBe("world"); + done(); + }) + }) + }); + }); + }); + }); + }); + }); + }); + }); it("unlink doesn't follow symlinks", done => { fs.mkdir("/symlink").finally(() => { fs.mkdir("/symlink/del").finally(() => { diff --git a/src/__tests__/fs.spec.js b/src/__tests__/fs.spec.js index 4ed58fa..bb4dfa3 100755 --- a/src/__tests__/fs.spec.js +++ b/src/__tests__/fs.spec.js @@ -292,6 +292,25 @@ describe("fs module", () => { }); }); }); + 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", () => { @@ -317,6 +336,33 @@ describe("fs module", () => { }); }); }); + 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", () => { From 34e49c25406f776293824e1ffb61fa2f3c312142 Mon Sep 17 00:00:00 2001 From: William Hilton Date: Thu, 14 Mar 2019 23:25:52 -0400 Subject: [PATCH 3/3] fix: remove stray console.logs --- src/CacheFS.js | 1 - src/IdbBackend.js | 2 +- src/__tests__/fs.promises.spec.js | 3 +-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/CacheFS.js b/src/CacheFS.js index 3c3434c..f0b2227 100755 --- a/src/CacheFS.js +++ b/src/CacheFS.js @@ -114,7 +114,6 @@ module.exports = class CacheFS { let target = stat.target if (!target.startsWith('/')) { target = path.normalize(path.join(partialPath, target)) - console.log('TARGET', target) } dir = this._lookup(target) } diff --git a/src/IdbBackend.js b/src/IdbBackend.js index a975cc1..a12afb7 100644 --- a/src/IdbBackend.js +++ b/src/IdbBackend.js @@ -42,7 +42,7 @@ module.exports = class IdbBackend { } else { // auto-expire locks after 24 hours if (value < (new Date().valueOf() - 24 * 60 * 60 * 1000)) { - console.log('lock is expired') + // console.log('lock is expired') value = undefined } // console.log(`${iam} ${call} denied`) diff --git a/src/__tests__/fs.promises.spec.js b/src/__tests__/fs.promises.spec.js index 14c464b..fbb9b41 100755 --- a/src/__tests__/fs.promises.spec.js +++ b/src/__tests__/fs.promises.spec.js @@ -11,7 +11,6 @@ describe("fs.promises module", () => { it("root directory already exists", (done) => { fs.mkdir("/").catch(err => { expect(err).not.toBe(null); - console.log(err) expect(err.code).toEqual("EEXIST"); done(); }); @@ -281,7 +280,7 @@ describe("fs.promises module", () => { }) }) }); - }).catch(console.log); + }); }); }); });