Skip to content

Commit 0524f11

Browse files
fuzzyTewbilliegoose
authored andcommitted
feat: add 'backFile' function to add http-backed files to superblock (#32)
1 parent f1419b8 commit 0524f11

File tree

8 files changed

+99
-15
lines changed

8 files changed

+99
-15
lines changed

README.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,11 @@ const fs = new FS("testfs")
5959

6060
Options object:
6161

62-
| Param | Type [= default] | Description |
63-
| ------ | ------------------ | --------------------------------------------------------------------- |
64-
| `wipe` | boolean = false | Delete the database and start with an empty filesystem |
65-
| `url` | string = undefined | Let `readFile` requests fall back to an HTTP request to this base URL |
62+
| Param | Type [= default] | Description |
63+
| --------- | ------------------ | --------------------------------------------------------------------- |
64+
| `wipe` | boolean = false | Delete the database and start with an empty filesystem |
65+
| `url` | string = undefined | Let `readFile` requests fall back to an HTTP request to this base URL |
66+
| `urlauto` | boolean = false | Fall back to HTTP for every read of a missing file, even if unbacked |
6667

6768
### `fs.mkdir(filepath, opts?, cb)`
6869

@@ -149,6 +150,17 @@ Create a symlink at `filepath` that points to `target`.
149150

150151
Read the target of a symlink.
151152

153+
### `fs.backFile(filepath, opts?, cb)`
154+
155+
Create or change the stat data for a file backed by HTTP. Size is fetched with a HEAD request. Useful when using an HTTP backend without `urlauto` set, as then files will only be readable if they have stat data.
156+
Note that stat data is made automatically from the file `/.superblock.txt` if found on the server. `/.superblock.txt` can be generated or updated with the [included standalone script](src/superblocktxt.js).
157+
158+
Options object:
159+
160+
| Param | Type [= default] | Description |
161+
| ---------- | ------------------ | -------------------------------- |
162+
| `mode` | number = 0o666 | Posix mode permissions |
163+
152164
### `fs.promises`
153165

154166
All the same functions as above, but instead of passing a callback they return a promise.

src/CacheFS.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ module.exports = class CacheFS {
165165
if (dir.get(STAT).type !== 'dir') throw new ENOTDIR();
166166
return [...dir.keys()].filter(key => typeof key === "string");
167167
}
168-
writeFile(filepath, data, { mode }) {
168+
writeStat(filepath, size, { mode }) {
169169
let ino;
170170
try {
171171
let oldStat = this.stat(filepath);
@@ -185,7 +185,7 @@ module.exports = class CacheFS {
185185
let stat = {
186186
mode,
187187
type: "file",
188-
size: data.length,
188+
size,
189189
mtimeMs: Date.now(),
190190
ino,
191191
};

src/HttpBackend.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module.exports = class HttpBackend {
33
this._url = url;
44
}
55
loadSuperblock() {
6-
return fetch(this._url + '/.superblock.txt').then(res => res.text())
6+
return fetch(this._url + '/.superblock.txt').then(res => res.ok ? res.text() : null)
77
}
88
async readFile(filepath) {
99
const res = await fetch(this._url + filepath)
@@ -13,4 +13,12 @@ module.exports = class HttpBackend {
1313
throw new Error('ENOENT')
1414
}
1515
}
16+
async sizeFile(filepath) {
17+
const res = await fetch(this._url + filepath, { method: 'HEAD' })
18+
if (res.status === 200) {
19+
return res.headers.get('content-length')
20+
} else {
21+
throw new Error('ENOENT')
22+
}
23+
}
1624
}

src/PromisifiedFS.js

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ function cleanParams2(oldFilepath, newFilepath) {
3333
}
3434

3535
module.exports = class PromisifiedFS {
36-
constructor(name, { wipe, url } = {}) {
36+
constructor(name, { wipe, url, urlauto } = {}) {
3737
this._name = name
3838
this._idb = new IdbBackend(name);
3939
this._mutex = new Mutex(name);
@@ -45,6 +45,7 @@ module.exports = class PromisifiedFS {
4545
}, 500);
4646
if (url) {
4747
this._http = new HttpBackend(url)
48+
this._urlauto = !!urlauto
4849
}
4950
this._operations = new Set()
5051

@@ -59,6 +60,7 @@ module.exports = class PromisifiedFS {
5960
this.lstat = this._wrap(this.lstat, false)
6061
this.readlink = this._wrap(this.readlink, false)
6162
this.symlink = this._wrap(this.symlink, true)
63+
this.backFile = this._wrap(this.backFile, true)
6264

6365
this._deactivationPromise = null
6466
this._deactivationTimeout = null
@@ -143,18 +145,41 @@ module.exports = class PromisifiedFS {
143145
await this._idb.saveSuperblock(this._cache._root);
144146
}
145147
}
148+
async _writeStat(filepath, size, opts) {
149+
let dirparts = path.split(path.dirname(filepath))
150+
let dir = dirparts.shift()
151+
for (let dirpart of dirparts) {
152+
dir = path.join(dir, dirpart)
153+
try {
154+
this._cache.mkdir(dir, { mode: 0o777 })
155+
} catch (e) {}
156+
}
157+
return this._cache.writeStat(filepath, size, opts)
158+
}
146159
async readFile(filepath, opts) {
147160
;[filepath, opts] = cleanParams(filepath, opts);
148161
const { encoding } = opts;
149162
if (encoding && encoding !== 'utf8') throw new Error('Only "utf8" encoding is supported in readFile');
150-
const stat = this._cache.stat(filepath);
151-
let data = await this._idb.readFile(stat.ino)
163+
let data = null, stat = null
164+
try {
165+
stat = this._cache.stat(filepath);
166+
data = await this._idb.readFile(stat.ino)
167+
} catch (e) {
168+
if (!this._urlauto) throw e
169+
}
152170
if (!data && this._http) {
153171
data = await this._http.readFile(filepath)
154172
}
155-
if (data && encoding === "utf8") {
156-
data = decode(data);
173+
if (data) {
174+
if (!stat || stat.size != data.byteLength) {
175+
stat = await this._writeStat(filepath, data.byteLength, { mode: stat ? stat.mode : 0o666 })
176+
this.saveSuperblock() // debounced
177+
}
178+
if (encoding === "utf8") {
179+
data = decode(data);
180+
}
157181
}
182+
if (!stat) throw new ENOENT(filepath)
158183
return data;
159184
}
160185
async writeFile(filepath, data, opts) {
@@ -166,7 +191,7 @@ module.exports = class PromisifiedFS {
166191
}
167192
data = encode(data);
168193
}
169-
const stat = this._cache.writeFile(filepath, data, { mode });
194+
const stat = await this._cache.writeStat(filepath, data.byteLength, { mode });
170195
await this._idb.writeFile(stat.ino, data)
171196
return null
172197
}
@@ -222,4 +247,10 @@ module.exports = class PromisifiedFS {
222247
this._cache.symlink(target, filepath);
223248
return null;
224249
}
250+
async backFile(filepath, opts) {
251+
;[filepath, opts] = cleanParams(filepath, opts);
252+
let size = await this._http.sizeFile(filepath)
253+
await this._writeStat(filepath, size, opts)
254+
return null
255+
}
225256
}

src/__tests__/CacheFS.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe("CacheFS module", () => {
2121
const fs = new CacheFS();
2222
fs.activate()
2323
expect(fs.autoinc()).toEqual(1)
24-
fs.writeFile('/foo', 'bar', {})
24+
fs.writeStat('/foo', 3, {})
2525
expect(fs.autoinc()).toEqual(2)
2626
fs.mkdir('/bar', {})
2727
expect(fs.autoinc()).toEqual(3)
@@ -33,7 +33,7 @@ describe("CacheFS module", () => {
3333
expect(fs.autoinc()).toEqual(3)
3434
fs.mkdir('/bar/bar', {})
3535
expect(fs.autoinc()).toEqual(4)
36-
fs.writeFile('/bar/bar/boo', 'bar', {})
36+
fs.writeStat('/bar/bar/boo', 3, {})
3737
expect(fs.autoinc()).toEqual(5)
3838
fs.unlink('/bar/bar/boo')
3939
expect(fs.autoinc()).toEqual(4)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Hello from "not-in-superblock"

src/__tests__/fallback.spec.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ describe("http fallback", () => {
3838
done();
3939
});
4040
});
41+
it("read file not in superblock throws", done => {
42+
fs.readFile("/not-in-superblock.txt", (err, data) => {
43+
expect(err).not.toBe(null);
44+
done();
45+
});
46+
});
4147
it("read file /a.txt", done => {
4248
fs.readFile("/a.txt", 'utf8', (err, data) => {
4349
expect(err).toBe(null);
@@ -82,4 +88,25 @@ describe("http fallback", () => {
8288
});
8389
});
8490
});
91+
describe("backFile", () => {
92+
it("backing a nonexistant file throws", done => {
93+
fs.backFile("/backFile/non-existant.txt", (err, data) => {
94+
expect(err).not.toBe(null);
95+
done();
96+
});
97+
});
98+
it("backing a file makes it readable", done => {
99+
fs.backFile("/not-in-superblock.txt", (err, data) => {
100+
expect(err).toBe(null)
101+
fs.readFile("/not-in-superblock.txt", 'utf8', (err, data) => {
102+
expect(err).toBe(null);
103+
expect(data).toEqual('Hello from "not-in-superblock"');
104+
fs.unlink("/not-in-superblock.txt", (err, data) => {
105+
expect(err).toBe(null);
106+
done();
107+
});
108+
});
109+
});
110+
});
111+
});
85112
});

src/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module.exports = class FS {
2626
this.lstat = this.lstat.bind(this)
2727
this.readlink = this.readlink.bind(this)
2828
this.symlink = this.symlink.bind(this)
29+
this.backFile = this.backFile.bind(this)
2930
}
3031
readFile(filepath, opts, cb) {
3132
const [resolve, reject] = wrapCallback(opts, cb);
@@ -71,4 +72,8 @@ module.exports = class FS {
7172
const [resolve, reject] = wrapCallback(cb);
7273
this.promises.symlink(target, filepath).then(resolve).catch(reject);
7374
}
75+
backFile(filepath, opts, cb) {
76+
const [resolve, reject] = wrapCallback(opts, cb);
77+
this.promises.backFile(filepath, opts).then(resolve).catch(reject);
78+
}
7479
}

0 commit comments

Comments
 (0)