Skip to content

Commit e47c9c1

Browse files
authored
feat: add fs.promises (#12)
1 parent 3c6e803 commit e47c9c1

File tree

5 files changed

+633
-250
lines changed

5 files changed

+633
-250
lines changed

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ I wanted to see if I could make something faster than [BrowserFS](https://github
99
## Comparison with other libraries
1010

1111
This library does not even come close to implementing the full [`fs`](https://nodejs.org/api/fs.html) API.
12-
Instead, it only implements [the subset used by isomorphic-git 'fs' plugin interface](https://isomorphic-git.org/docs/en/plugin_fs).
12+
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.
1313

1414
Unlike BrowserFS, which has a dozen backends and is highly configurable, `lightning-fs` has a single configuration that should Just Work for most users.
1515

@@ -131,7 +131,19 @@ The included methods are:
131131

132132
### `fs.lstat(filepath, opts?, cb)`
133133

134-
Alias to `fs.stat` for now until symlinks are supported.
134+
Like `fs.stat` except that paths to symlinks return the symlink stats not the file stats of the symlink's target.
135+
136+
### `fs.symlink(target, filepath, cb)`
137+
138+
Create a symlink at `filepath` that points to `target`.
139+
140+
### `fs.readlink(filepath, opts?, cb)`
141+
142+
Read the target of a symlink.
143+
144+
### `fs.promises`
145+
146+
All the same functions as above, but instead of passing a callback they return a promise.
135147

136148
## License
137149

src/PromisifiedFS.js

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
const { encode, decode } = require("isomorphic-textencoder");
2+
const debounce = require("just-debounce-it");
3+
4+
const Stat = require("./Stat.js");
5+
const CacheFS = require("./CacheFS.js");
6+
const { ENOENT, ENOTEMPTY } = require("./errors.js");
7+
const IdbBackend = require("./IdbBackend.js");
8+
const HttpBackend = require("./HttpBackend.js")
9+
10+
const path = require("./path.js");
11+
const clock = require("./clock.js");
12+
13+
function cleanParams(filepath, opts) {
14+
// normalize paths
15+
filepath = path.normalize(filepath);
16+
// strip out callbacks
17+
if (typeof opts === "undefined" || typeof opts === "function") {
18+
opts = {};
19+
}
20+
// expand string options to encoding options
21+
if (typeof opts === "string") {
22+
opts = {
23+
encoding: opts,
24+
};
25+
}
26+
return [filepath, opts];
27+
}
28+
29+
function cleanParams2(oldFilepath, newFilepath) {
30+
// normalize paths
31+
return [path.normalize(oldFilepath), path.normalize(newFilepath)];
32+
}
33+
34+
module.exports = class PromisifiedFS {
35+
constructor(name, { wipe, url } = {}) {
36+
this._idb = new IdbBackend(name);
37+
this._cache = new CacheFS(name);
38+
this._opts = { wipe, url };
39+
this.saveSuperblock = debounce(() => {
40+
this._saveSuperblock();
41+
}, 500);
42+
if (url) {
43+
this._http = new HttpBackend(url)
44+
}
45+
this._initPromise = this._init()
46+
// Needed so things don't break if you destructure fs and pass individual functions around
47+
this.readFile = this.readFile.bind(this)
48+
this.writeFile = this.writeFile.bind(this)
49+
this.unlink = this.unlink.bind(this)
50+
this.readdir = this.readdir.bind(this)
51+
this.mkdir = this.mkdir.bind(this)
52+
this.rmdir = this.rmdir.bind(this)
53+
this.rename = this.rename.bind(this)
54+
this.stat = this.stat.bind(this)
55+
this.lstat = this.lstat.bind(this)
56+
this.readlink = this.readlink.bind(this)
57+
this.symlink = this.symlink.bind(this)
58+
}
59+
async _init() {
60+
if (this._initPromise) return this._initPromise
61+
if (this._opts.wipe) {
62+
await this._wipe();
63+
} else {
64+
await this._loadSuperblock();
65+
}
66+
}
67+
_wipe() {
68+
return this._idb.wipe().then(() => {
69+
if (this._http) {
70+
return this._http.loadSuperblock().then(text => {
71+
if (text) {
72+
this._cache.loadSuperBlock(text)
73+
}
74+
})
75+
}
76+
}).then(() => this._saveSuperblock());
77+
}
78+
_saveSuperblock() {
79+
return this._idb.saveSuperblock(this._cache._root);
80+
}
81+
_loadSuperblock() {
82+
return this._idb.loadSuperblock().then(root => {
83+
if (root) {
84+
this._cache.loadSuperBlock(root);
85+
} else if (this._http) {
86+
return this._http.loadSuperblock().then(text => {
87+
if (text) {
88+
this._cache.loadSuperBlock(text)
89+
}
90+
})
91+
}
92+
});
93+
}
94+
async readFile(filepath, opts) {
95+
await this._init()
96+
;[filepath, opts] = cleanParams(filepath, opts);
97+
const { encoding } = opts;
98+
if (encoding && encoding !== 'utf8') throw new Error('Only "utf8" encoding is supported in readFile');
99+
const stat = this._cache.stat(filepath);
100+
let data = await this._idb.readFile(stat.ino)
101+
if (!data && this._http) {
102+
data = await this._http.readFile(filepath)
103+
}
104+
if (data && encoding === "utf8") {
105+
data = decode(data);
106+
}
107+
return data;
108+
}
109+
async writeFile(filepath, data, opts) {
110+
await this._init()
111+
;[filepath, opts] = cleanParams(filepath, opts);
112+
const { mode, encoding = "utf8" } = opts;
113+
if (typeof data === "string") {
114+
if (encoding !== "utf8") {
115+
throw new Error('Only "utf8" encoding is supported in writeFile');
116+
}
117+
data = encode(data);
118+
}
119+
const stat = this._cache.writeFile(filepath, data, { mode });
120+
await this._idb.writeFile(stat.ino, data)
121+
this.saveSuperblock();
122+
return null
123+
}
124+
async unlink(filepath, opts) {
125+
await this._init()
126+
;[filepath, opts] = cleanParams(filepath, opts);
127+
const stat = this._cache.stat(filepath);
128+
this._cache.unlink(filepath);
129+
await this._idb.unlink(stat.ino)
130+
this.saveSuperblock();
131+
return null
132+
}
133+
async readdir(filepath, opts) {
134+
await this._init()
135+
;[filepath, opts] = cleanParams(filepath, opts);
136+
return this._cache.readdir(filepath);
137+
}
138+
async mkdir(filepath, opts) {
139+
await this._init()
140+
;[filepath, opts] = cleanParams(filepath, opts);
141+
const { mode = 0o777 } = opts;
142+
await this._cache.mkdir(filepath, { mode });
143+
this.saveSuperblock();
144+
return null
145+
}
146+
async rmdir(filepath, opts) {
147+
await this._init()
148+
;[filepath, opts] = cleanParams(filepath, opts);
149+
// Never allow deleting the root directory.
150+
if (filepath === "/") {
151+
throw new ENOTEMPTY();
152+
}
153+
this._cache.rmdir(filepath);
154+
this.saveSuperblock();
155+
return null;
156+
}
157+
async rename(oldFilepath, newFilepath) {
158+
await this._init()
159+
;[oldFilepath, newFilepath] = cleanParams2(oldFilepath, newFilepath);
160+
this._cache.rename(oldFilepath, newFilepath);
161+
this.saveSuperblock();
162+
return null;
163+
}
164+
async stat(filepath, opts) {
165+
await this._init()
166+
;[filepath, opts] = cleanParams(filepath, opts);
167+
const data = this._cache.stat(filepath);
168+
return new Stat(data);
169+
}
170+
async lstat(filepath, opts) {
171+
await this._init()
172+
;[filepath, opts] = cleanParams(filepath, opts);
173+
let data = this._cache.lstat(filepath);
174+
return new Stat(data);
175+
}
176+
async readlink(filepath, opts) {
177+
await this._init()
178+
;[filepath, opts] = cleanParams(filepath, opts);
179+
return this._cache.readlink(filepath);
180+
}
181+
async symlink(target, filepath) {
182+
await this._init()
183+
;[target, filepath] = cleanParams2(target, filepath);
184+
this._cache.symlink(target, filepath);
185+
this.saveSuperblock();
186+
return null;
187+
}
188+
}

src/__tests__/fallback.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ const fs = new FS("fallbackfs", { wipe: true, url: 'http://localhost:9876/base/s
44

55
describe("http fallback", () => {
66
it("sanity check", () => {
7-
expect(fs._fallback).not.toBeFalsy()
7+
expect(fs.promises._http).not.toBeFalsy()
88
})
99
it("loads", (done) => {
10-
fs.superblockPromise.then(() => {
10+
fs.promises._init().then(() => {
1111
done()
1212
}).catch(err => {
1313
expect(err).toBe(null)

0 commit comments

Comments
 (0)