diff --git a/README.md b/README.md index 7cd28cc..55c8be4 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ I wanted to see if I could make something faster than [BrowserFS](https://github ## Comparison with other libraries This library does not even come close to implementing the full [`fs`](https://nodejs.org/api/fs.html) API. -Instead, it only implements [the subset used by isomorphic-git 'fs' plugin interface](https://isomorphic-git.org/docs/en/plugin_fs). +Instead, it only implements [the subset used by isomorphic-git 'fs' plugin interface](https://isomorphic-git.org/docs/en/plugin_fs) plus the [`fs.promises`](https://nodejs.org/dist/latest-v10.x/docs/api/fs.html#fs_fs_promises_api) versions of those functions. Unlike BrowserFS, which has a dozen backends and is highly configurable, `lightning-fs` has a single configuration that should Just Work for most users. @@ -131,7 +131,19 @@ The included methods are: ### `fs.lstat(filepath, opts?, cb)` -Alias to `fs.stat` for now until symlinks are supported. +Like `fs.stat` except that paths to symlinks return the symlink stats not the file stats of the symlink's target. + +### `fs.symlink(target, filepath, cb)` + +Create a symlink at `filepath` that points to `target`. + +### `fs.readlink(filepath, opts?, cb)` + +Read the target of a symlink. + +### `fs.promises` + +All the same functions as above, but instead of passing a callback they return a promise. ## License diff --git a/src/PromisifiedFS.js b/src/PromisifiedFS.js new file mode 100644 index 0000000..43eaa1b --- /dev/null +++ b/src/PromisifiedFS.js @@ -0,0 +1,188 @@ +const { encode, decode } = require("isomorphic-textencoder"); +const debounce = require("just-debounce-it"); + +const Stat = require("./Stat.js"); +const CacheFS = require("./CacheFS.js"); +const { ENOENT, ENOTEMPTY } = require("./errors.js"); +const IdbBackend = require("./IdbBackend.js"); +const HttpBackend = require("./HttpBackend.js") + +const path = require("./path.js"); +const clock = require("./clock.js"); + +function cleanParams(filepath, opts) { + // normalize paths + filepath = path.normalize(filepath); + // strip out callbacks + if (typeof opts === "undefined" || typeof opts === "function") { + opts = {}; + } + // expand string options to encoding options + if (typeof opts === "string") { + opts = { + encoding: opts, + }; + } + return [filepath, opts]; +} + +function cleanParams2(oldFilepath, newFilepath) { + // normalize paths + return [path.normalize(oldFilepath), path.normalize(newFilepath)]; +} + +module.exports = class PromisifiedFS { + constructor(name, { wipe, url } = {}) { + this._idb = new IdbBackend(name); + this._cache = new CacheFS(name); + this._opts = { wipe, url }; + this.saveSuperblock = debounce(() => { + this._saveSuperblock(); + }, 500); + if (url) { + this._http = new HttpBackend(url) + } + this._initPromise = this._init() + // 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) + this.unlink = this.unlink.bind(this) + this.readdir = this.readdir.bind(this) + this.mkdir = this.mkdir.bind(this) + this.rmdir = this.rmdir.bind(this) + 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) + } + async _init() { + if (this._initPromise) return this._initPromise + if (this._opts.wipe) { + await this._wipe(); + } else { + await this._loadSuperblock(); + } + } + _wipe() { + return this._idb.wipe().then(() => { + if (this._http) { + return this._http.loadSuperblock().then(text => { + if (text) { + this._cache.loadSuperBlock(text) + } + }) + } + }).then(() => this._saveSuperblock()); + } + _saveSuperblock() { + return this._idb.saveSuperblock(this._cache._root); + } + _loadSuperblock() { + return this._idb.loadSuperblock().then(root => { + if (root) { + this._cache.loadSuperBlock(root); + } else if (this._http) { + return this._http.loadSuperblock().then(text => { + if (text) { + this._cache.loadSuperBlock(text) + } + }) + } + }); + } + async readFile(filepath, opts) { + await this._init() + ;[filepath, opts] = cleanParams(filepath, opts); + const { encoding } = opts; + if (encoding && encoding !== 'utf8') throw new Error('Only "utf8" encoding is supported in readFile'); + const stat = this._cache.stat(filepath); + let data = await this._idb.readFile(stat.ino) + if (!data && this._http) { + data = await this._http.readFile(filepath) + } + if (data && encoding === "utf8") { + data = decode(data); + } + return data; + } + async writeFile(filepath, data, opts) { + await this._init() + ;[filepath, opts] = cleanParams(filepath, opts); + const { mode, encoding = "utf8" } = opts; + if (typeof data === "string") { + if (encoding !== "utf8") { + throw new Error('Only "utf8" encoding is supported in writeFile'); + } + data = encode(data); + } + const stat = this._cache.writeFile(filepath, data, { mode }); + await this._idb.writeFile(stat.ino, data) + this.saveSuperblock(); + return null + } + async unlink(filepath, opts) { + await this._init() + ;[filepath, opts] = cleanParams(filepath, opts); + const stat = this._cache.stat(filepath); + this._cache.unlink(filepath); + await this._idb.unlink(stat.ino) + this.saveSuperblock(); + return null + } + async readdir(filepath, opts) { + await this._init() + ;[filepath, opts] = cleanParams(filepath, opts); + return this._cache.readdir(filepath); + } + async mkdir(filepath, opts) { + await this._init() + ;[filepath, opts] = cleanParams(filepath, opts); + const { mode = 0o777 } = opts; + await this._cache.mkdir(filepath, { mode }); + this.saveSuperblock(); + return null + } + async rmdir(filepath, opts) { + await this._init() + ;[filepath, opts] = cleanParams(filepath, opts); + // Never allow deleting the root directory. + if (filepath === "/") { + throw new ENOTEMPTY(); + } + this._cache.rmdir(filepath); + this.saveSuperblock(); + return null; + } + async rename(oldFilepath, newFilepath) { + await this._init() + ;[oldFilepath, newFilepath] = cleanParams2(oldFilepath, newFilepath); + this._cache.rename(oldFilepath, newFilepath); + this.saveSuperblock(); + return null; + } + async stat(filepath, opts) { + await this._init() + ;[filepath, opts] = cleanParams(filepath, opts); + const data = this._cache.stat(filepath); + return new Stat(data); + } + async lstat(filepath, opts) { + await this._init() + ;[filepath, opts] = cleanParams(filepath, opts); + let data = this._cache.lstat(filepath); + return new Stat(data); + } + async readlink(filepath, opts) { + await this._init() + ;[filepath, opts] = cleanParams(filepath, opts); + return this._cache.readlink(filepath); + } + async symlink(target, filepath) { + await this._init() + ;[target, filepath] = cleanParams2(target, filepath); + this._cache.symlink(target, filepath); + this.saveSuperblock(); + return null; + } +} diff --git a/src/__tests__/fallback.spec.js b/src/__tests__/fallback.spec.js index 1451a50..f09f901 100644 --- a/src/__tests__/fallback.spec.js +++ b/src/__tests__/fallback.spec.js @@ -4,10 +4,10 @@ const fs = new FS("fallbackfs", { wipe: true, url: 'http://localhost:9876/base/s describe("http fallback", () => { it("sanity check", () => { - expect(fs._fallback).not.toBeFalsy() + expect(fs.promises._http).not.toBeFalsy() }) it("loads", (done) => { - fs.superblockPromise.then(() => { + fs.promises._init().then(() => { done() }).catch(err => { expect(err).toBe(null) diff --git a/src/__tests__/fs.promises.spec.js b/src/__tests__/fs.promises.spec.js new file mode 100755 index 0000000..3b07509 --- /dev/null +++ b/src/__tests__/fs.promises.spec.js @@ -0,0 +1,394 @@ +import FS from "../index.js"; + +const fs = new FS("testfs", { wipe: true }).promises; + +const HELLO = new Uint8Array([72, 69, 76, 76, 79]); + +if (!Promise.prototype.finally) { + Promise.prototype.finally = function (onFinally) { + this.then(onFinally, onFinally); + } +} + +describe("fs.promises module", () => { + describe("mkdir", () => { + it("root directory already exists", (done) => { + fs.mkdir("/").catch(err => { + expect(err).not.toBe(null); + expect(err.code).toEqual("EEXIST"); + done(); + }); + }); + it("create empty directory", done => { + fs.mkdir("/mkdir-test") + .then(() => { + fs.stat("/mkdir-test").then(stat => { + done(); + }); + }) + .catch(err => { + expect(err.code).toEqual("EEXIST"); + done(); + }); + }); + }); + + describe("writeFile", () => { + it("create file", done => { + fs.mkdir("/writeFile").finally(() => { + fs.writeFile("/writeFile/writeFile-uint8.txt", HELLO).then(() => { + fs.stat("/writeFile/writeFile-uint8.txt").then(stats => { + expect(stats.size).toEqual(5); + done(); + }); + }); + }); + }); + it("create file (from string)", done => { + fs.mkdir("/writeFile").finally(() => { + fs.writeFile("/writeFile/writeFile-string.txt", "HELLO").then(() => { + fs.stat("/writeFile/writeFile-string.txt").then(stats => { + expect(stats.size).toEqual(5); + done(); + }); + }); + }); + }); + it("write file perserves old inode", done => { + fs.mkdir("/writeFile").finally(() => { + fs.writeFile("/writeFile/writeFile-inode.txt", "HELLO").then(() => { + fs.stat("/writeFile/writeFile-inode.txt").then(stats => { + let inode = stats.ino; + fs.writeFile("/writeFile/writeFile-inode.txt", "WORLD").then(() => { + fs.stat("/writeFile/writeFile-inode.txt").then(stats => { + expect(stats.ino).toEqual(inode); + done(); + }); + }); + }); + }); + }); + }); + }); + + describe("readFile", () => { + it("read non-existant file throws", done => { + fs.readFile("/readFile/non-existant.txt").catch(err => { + expect(err).not.toBe(null); + done(); + }); + }); + it("read file", done => { + fs.mkdir("/readFile").finally(() => { + fs.writeFile("/readFile/readFile-uint8.txt", "HELLO").then(() => { + fs.readFile("/readFile/readFile-uint8.txt").then(data => { + // instanceof comparisons on Uint8Array's retrieved from IDB are broken in Safari Mobile 11.x (source: https://github.com/dfahlander/Dexie.js/issues/656#issuecomment-391866600) + expect([...data]).toEqual([...HELLO]); + done(); + }); + }); + }); + }); + it("read file (encoding shorthand)", done => { + fs.mkdir("/readFile").finally(() => { + fs.writeFile("/readFile/readFile-encoding-shorthand.txt", "HELLO").then(() => { + fs.readFile("/readFile/readFile-encoding-shorthand.txt", "utf8").then(data => { + expect(data).toEqual("HELLO"); + done(); + }); + }); + }); + }); + it("read file (encoding longhand)", done => { + fs.mkdir("/readFile").finally(() => { + fs.writeFile("/readFile/readFile-encoding-longhand.txt", "HELLO").then(() => { + fs.readFile("/readFile/readFile-encoding-longhand.txt", { encoding: "utf8" }).then(data => { + expect(data).toEqual("HELLO"); + done(); + }); + }); + }); + }); + }); + + describe("readdir", () => { + it("read non-existant dir returns undefined", done => { + fs.readdir("/readdir/non-existant").catch(err => { + expect(err).not.toBe(null); + done(); + }); + }); + it("read root directory", done => { + fs.mkdir("/readdir").finally(() => { + fs.readdir("/").then(data => { + expect(data.includes("readdir")).toBe(true); + done(); + }); + }); + }); + it("read child directory", done => { + fs.mkdir("/readdir").finally(() => { + fs.writeFile("/readdir/1.txt", "").then(() => { + fs.readdir("/readdir").then(data => { + expect(data).toEqual(["1.txt"]) + done(); + }); + }); + }); + }); + }); + + describe("rmdir", () => { + it("delete root directory fails", done => { + fs.rmdir("/").catch(err => { + expect(err).not.toBe(null); + expect(err.code).toEqual("ENOTEMPTY"); + done(); + }); + }); + it("delete non-existant directory fails", done => { + fs.rmdir("/rmdir/non-existant").catch(err => { + expect(err).not.toBe(null); + expect(err.code).toEqual("ENOENT"); + done(); + }); + }); + it("delete non-empty directory fails", done => { + fs.mkdir("/rmdir").finally(() => { + fs.mkdir("/rmdir/not-empty").finally(() => { + fs.writeFile("/rmdir/not-empty/file.txt", "").then(() => { + + fs.rmdir("/rmdir/not-empty").catch(err => { + expect(err).not.toBe(null); + expect(err.code).toEqual("ENOTEMPTY"); + done(); + }); + }) + }) + }) + }); + it("delete empty directory", done => { + fs.mkdir("/rmdir").finally(() => { + fs.mkdir("/rmdir/empty").finally(() => { + fs.readdir("/rmdir").then(data => { + let originalSize = data.length; + fs.rmdir("/rmdir/empty").then(() => { + fs.readdir("/rmdir").then(data => { + expect(data.length === originalSize - 1); + expect(data.includes("empty")).toBe(false); + done(); + }); + }); + }); + }); + }); + }); + }); + + describe("unlink", () => { + it("create and delete file", done => { + fs.mkdir("/unlink").finally(() => { + fs.writeFile("/unlink/file.txt", "").then(() => { + fs.readdir("/unlink").then(data => { + let originalSize = data.length; + fs.unlink("/unlink/file.txt").then(() => { + fs.readdir("/unlink").then(data => { + expect(data.length).toBe(originalSize - 1) + expect(data.includes("file.txt")).toBe(false); + fs.readFile("/unlink/file.txt").catch(err => { + expect(err).not.toBe(null) + expect(err.code).toBe("ENOENT") + done(); + }); + }); + }); + }); + }); + }); + }); + }); + + describe("rename", () => { + it("create and rename file", done => { + fs.mkdir("/rename").finally(() => { + fs.writeFile("/rename/a.txt", "").then(() => { + fs.rename("/rename/a.txt", "/rename/b.txt").then(() => { + fs.readdir("/rename").then(data => { + expect(data.includes("a.txt")).toBe(false); + expect(data.includes("b.txt")).toBe(true); + fs.readFile("/rename/a.txt").catch(err => { + expect(err).not.toBe(null) + expect(err.code).toBe("ENOENT") + fs.readFile("/rename/b.txt", "utf8").then(data => { + expect(data).toBe("") + done(); + }); + }); + }); + }); + }); + }); + }); + it("create and rename directory", done => { + fs.mkdir("/rename").finally(() => { + fs.mkdir("/rename/a").finally(() => { + fs.writeFile("/rename/a/file.txt", "").then(() => { + fs.rename("/rename/a", "/rename/b").then(() => { + fs.readdir("/rename").then(data => { + expect(data.includes("a")).toBe(false); + expect(data.includes("b")).toBe(true); + fs.readFile("/rename/a/file.txt").catch(err => { + expect(err).not.toBe(null) + expect(err.code).toBe("ENOENT") + fs.readFile("/rename/b/file.txt", "utf8").then(data => { + expect(data).toBe("") + done(); + }); + }); + }); + }); + }); + }); + }); + }); + }); + + 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 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(); + }) + }) + }); + }); + }); + }); + }); + 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("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(() => { + 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/index.js b/src/index.js index a4abbaa..70e3c38 100755 --- a/src/index.js +++ b/src/index.js @@ -1,285 +1,74 @@ const once = require("just-once"); -const { encode, decode } = require("isomorphic-textencoder"); -const debounce = require("just-debounce-it"); -const path = require("./path.js"); -const Stat = require("./Stat.js"); -const CacheFS = require("./CacheFS.js"); -const { ENOENT, ENOTEMPTY } = require("./errors.js"); -const IdbBackend = require("./IdbBackend.js"); -const HttpBackend = require("./HttpBackend.js") -const clock = require("./clock.js"); +const PromisifiedFS = require('./PromisifiedFS'); + +function wrapCallback (opts, cb) { + if (typeof opts === "function") { + cb = opts; + } + cb = once(cb); + const resolve = (...args) => cb(null, ...args) + return [resolve, cb]; +} module.exports = class FS { - constructor(name, { wipe, url } = {}) { - this._backend = new IdbBackend(name); - this._cache = new CacheFS(name); - this.saveSuperblock = debounce(() => { - this._saveSuperblock(); - }, 500); - if (url) { - this._fallback = new HttpBackend(url) - } - if (wipe) { - this.superblockPromise = this._wipe(); - } else { - this.superblockPromise = this._loadSuperblock(); - } + constructor(...args) { + this.promises = new PromisifiedFS(...args) // 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) this.unlink = this.unlink.bind(this) + this.readdir = this.readdir.bind(this) this.mkdir = this.mkdir.bind(this) this.rmdir = this.rmdir.bind(this) - this.readdir = this.readdir.bind(this) 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); - if (typeof opts === "function") { - cb = opts; - opts = {}; - } - if (typeof opts === "string") { - opts = { - encoding: opts, - }; - } - const _cb = cb; - cb = once((...args) => { - if (stopClock) stopClock(); - if (save) this.saveSuperblock(); - _cb(...args); - }); - return [filepath, opts, cb]; - } - _cleanParams2(oldFilepath, newFilepath, cb, stopClock = null, save = false) { - oldFilepath = path.normalize(oldFilepath); - newFilepath = path.normalize(newFilepath); - const _cb = cb; - cb = once((...args) => { - if (stopClock) stopClock(); - if (save) this.saveSuperblock(); - _cb(...args); - }); - return [oldFilepath, newFilepath, cb]; - } - _wipe() { - return this._backend.wipe().then(() => { - if (this._fallback) { - return this._fallback.loadSuperblock().then(text => { - if (text) { - this._cache.loadSuperBlock(text) - } - }) - } - }).then(() => this._saveSuperblock()); - } - _saveSuperblock() { - return this._backend.saveSuperblock(this._cache._root); - } - _loadSuperblock() { - return this._backend.loadSuperblock().then(root => { - if (root) { - this._cache.loadSuperBlock(root); - } else if (this._fallback) { - return this._fallback.loadSuperblock().then(text => { - if (text) { - this._cache.loadSuperBlock(text) - } - }) - } - }); - } readFile(filepath, opts, cb) { - const stopClock = clock(`readFile ${filepath}`); - [filepath, opts, cb] = this._cleanParams(filepath, opts, cb, stopClock); - - const { encoding } = opts; - this.superblockPromise - .then(() => { - let stat - try { - stat = this._cache.stat(filepath); - } catch (err) { - return cb(err); - } - this._backend.readFile(stat.ino) - .then(data => { - if (data || !this._fallback) { - return data - } else { - return this._fallback.readFile(filepath) - } - }) - .then(data => { - if (data) { - if (encoding === "utf8") { - data = decode(data); - } - } - cb(null, data); - }) - .catch(err => { - console.log("ERROR: readFile: stat data out of sync with db:", filepath); - }); - }) - .catch(cb); + const [resolve, reject] = wrapCallback(opts, cb); + this.promises.readFile(filepath, opts).then(resolve).catch(reject) } writeFile(filepath, data, opts, cb) { - let stop = clock(`writeFile ${filepath}`); - [filepath, opts, cb] = this._cleanParams(filepath, opts, cb, stop, true); - - const { mode, encoding = "utf8" } = opts; - if (typeof data === "string") { - if (encoding !== "utf8") { - return cb(new Error('Only "utf8" encoding is supported in writeFile')); - } - data = encode(data); - } - this.superblockPromise - .then(() => { - let stat - try { - stat = this._cache.writeFile(filepath, data, { mode }); - } catch (err) { - return cb(err); - } - this._backend.writeFile(stat.ino, data) - .then(() => cb(null)) - .catch(err => cb(err)); - }) - .catch(cb); + const [resolve, reject] = wrapCallback(opts, cb); + this.promises.writeFile(filepath, data, opts).then(resolve).catch(reject); } unlink(filepath, opts, cb) { - let stop = clock(`unlink ${filepath}`); - [filepath, opts, cb] = this._cleanParams(filepath, opts, cb, stop, true); - this.superblockPromise - .then(() => { - let stat - try { - stat = this._cache.stat(filepath); - this._cache.unlink(filepath); - } catch (err) { - return cb(err); - } - this._backend.unlink(stat.ino) - .then(() => cb(null)) - .catch(cb); - }) - .catch(cb); + const [resolve, reject] = wrapCallback(opts, cb); + this.promises.unlink(filepath, opts).then(resolve).catch(reject); } readdir(filepath, opts, cb) { - [filepath, opts, cb] = this._cleanParams(filepath, opts, cb); - this.superblockPromise - .then(() => { - try { - let data = this._cache.readdir(filepath); - return cb(null, data); - } catch (err) { - return cb(err); - } - }) - .catch(cb); + const [resolve, reject] = wrapCallback(opts, cb); + this.promises.readdir(filepath, opts).then(resolve).catch(reject); } mkdir(filepath, opts, cb) { - [filepath, opts, cb] = this._cleanParams(filepath, opts, cb, null, true); - const { mode = 0o777 } = opts; - this.superblockPromise - .then(() => { - try { - this._cache.mkdir(filepath, { mode }); - return cb(null); - } catch (err) { - return cb(err); - } - }) - .catch(cb); + const [resolve, reject] = wrapCallback(opts, cb); + this.promises.mkdir(filepath, opts).then(resolve).catch(reject) } rmdir(filepath, opts, cb) { - [filepath, opts, cb] = this._cleanParams(filepath, opts, cb, null, true); - // Never allow deleting the root directory. - if (filepath === "/") { - return cb(new ENOTEMPTY()); - } - this.superblockPromise - .then(() => { - try { - this._cache.rmdir(filepath); - return cb(null); - } catch (err) { - return cb(err); - } - }) - .catch(cb); + const [resolve, reject] = wrapCallback(opts, cb); + this.promises.rmdir(filepath, opts).then(resolve).catch(reject) } rename(oldFilepath, newFilepath, cb) { - [oldFilepath, newFilepath, cb] = this._cleanParams2(oldFilepath, newFilepath, cb, null, true); - this.superblockPromise - .then(() => { - try { - this._cache.rename(oldFilepath, newFilepath); - return cb(null); - } catch (err) { - return cb(err); - } - }) - .catch(cb); + const [resolve, reject] = wrapCallback(cb); + this.promises.rename(oldFilepath, newFilepath).then(resolve).catch(reject) } stat(filepath, opts, cb) { - [filepath, opts, cb] = this._cleanParams(filepath, opts, cb); - this.superblockPromise - .then(() => { - try { - let data = this._cache.stat(filepath); - return cb(null, new Stat(data)); - } catch (err) { - return cb(err); - } - }) - .catch(cb); + const [resolve, reject] = wrapCallback(opts, cb); + this.promises.stat(filepath).then(resolve).catch(reject); } lstat(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); + const [resolve, reject] = wrapCallback(opts, cb); + this.promises.lstat(filepath).then(resolve).catch(reject); } 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); + const [resolve, reject] = wrapCallback(opts, cb); + this.promises.readlink(filepath).then(resolve).catch(reject); } 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); + const [resolve, reject] = wrapCallback(cb); + this.promises.symlink(target, filepath).then(resolve).catch(reject); } }