Skip to content

Commit 074547c

Browse files
authored
feat: add 'rename' function
BREAKING CHANGE: The IndexedDB database format changed - the primary key is now an inode number rather than a filepath string. Adding this layer of indirection make renaming files and directories fast. (Before, renaming a directory would have required recursively copying all the files to a new location.) Rather than bloat the code with a migration script, I recommend simply creating a fresh filesystem or blowing the old filesystem away with the `wipe` argument. Maybe you can load v2 and v3 at the same time, and recursively read from the v2 instance and write to the v3 instance? Database migrations are hard, and I apologize. But this should be the first and last backwards incompatible change to the database format. BREAKING CHANGE: the `stat` function now returns `mtimeMs` rather than `mtimeSeconds` and `mtimeNanoseconds` to match what Node's `stat` function returns instead of catering to an implementation detail of `isomorphic-git`.
1 parent 1ffdbd3 commit 074547c

File tree

8 files changed

+226
-44
lines changed

8 files changed

+226
-44
lines changed

README.md

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Req #3 excludes pure in-memory solutions. Req #4 excludes `localStorage` because
3131
3. memory usage (will it work on mobile)
3232

3333
In order to get improve #1, I ended up making a hybrid in-memory / IndexedDB system:
34-
- `mkdir`, `rmdir`, `readdir`, and `stat` are pure in-memory operations that take 0ms
34+
- `mkdir`, `rmdir`, `readdir`, `rename`, and `stat` are pure in-memory operations that take 0ms
3535
- `writeFile`, `readFile`, and `unlink` are throttled by IndexedDB
3636

3737
The in-memory portion of the filesystem is persisted to IndexedDB with a debounce of 500ms.
@@ -40,7 +40,94 @@ Applications can always *add* an LRU cache on top of `lightning-fs` - if I add o
4040

4141
## Usage
4242

43-
TODO
43+
### `new FS(name, opts?)`
44+
First, create or open a "filesystem". (The name is used to determine the IndexedDb store name.)
45+
46+
```
47+
import FS from '@isomorphic-git/lightning-fs';
48+
49+
const fs = new FS("testfs")
50+
```
51+
52+
Options object:
53+
54+
| Param | Type [= default] | Description |
55+
| ------ | ------------------ | --------------------------------------------------------------------- |
56+
| `wipe` | boolean = false | Delete the database and start with an empty filesystem |
57+
| `url` | string = undefined | Let `readFile` requests fall back to an HTTP request to this base URL |
58+
59+
### `fs.mkdir(filepath, opts?, cb)`
60+
61+
Make directory
62+
63+
Options object:
64+
65+
| Param | Type [= default] | Description |
66+
| ------ | ---------------- | ---------------------- |
67+
| `mode` | number = 0o777 | Posix mode permissions |
68+
69+
### `fs.rmdir(filepath, opts?, cb)`
70+
71+
Remove directory
72+
73+
### `fs.readdir(filepath, opts?, cb)`
74+
75+
Read directory
76+
77+
The callback return value is an Array of strings.
78+
79+
### `fs.writeFile(filepath, data, opts?, cb)`
80+
81+
`data` should be a string of a Uint8Array.
82+
83+
If `opts` is a string, it is interpreted as `{ encoding: opts }`.
84+
85+
Options object:
86+
87+
| Param | Type [= default] | Description |
88+
| ---------- | ------------------ | -------------------------------- |
89+
| `mode` | number = 0o777 | Posix mode permissions |
90+
| `encoding` | string = undefined | Only supported value is `'utf8'` |
91+
92+
### `fs.readFile(filepath, opts?, cb)`
93+
94+
The result value will be a Uint8Array or (if `encoding` is `'utf8'`) a string.
95+
96+
If `opts` is a string, it is interpreted as `{ encoding: opts }`.
97+
98+
Options object:
99+
100+
| Param | Type [= default] | Description |
101+
| ---------- | ------------------ | -------------------------------- |
102+
| `encoding` | string = undefined | Only supported value is `'utf8'` |
103+
104+
### `fs.unlink(filepath, opts?, cb)`
105+
106+
Delete a file
107+
108+
### `fs.rename(oldFilepath, newFilepath, cb)`
109+
110+
Rename a file or directory
111+
112+
### `fs.stat(filepath, opts?, cb)`
113+
114+
The result is a Stat object similar to the one used by Node but with fewer and slightly different properties and methods.
115+
The included properties are:
116+
117+
- `type` ("file" or "dir")
118+
- `mode`
119+
- `size`
120+
- `ino`
121+
- `mtimeMs`
122+
- `ctimeMs`
123+
- `uid` (fixed value of 1)
124+
- `gid` (fixed value of 1)
125+
- `dev` (fixed value of 1)
126+
127+
The included methods are:
128+
- `isFile()`
129+
- `isDirectory()`
130+
- `isSymbolicLink()`
44131

45132
## License
46133

src/CacheFS.js

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,32 @@ module.exports = class CacheFS {
1818
this._root = superblock
1919
}
2020
}
21+
size () {
22+
// subtract 1 to ignore the root directory itself from the count.
23+
return this._countInodes(this._root.get("/")) - 1;
24+
}
25+
_countInodes(map) {
26+
let count = 1;
27+
for (let [key, val] of map) {
28+
if (key === STAT) continue;
29+
count += this._countInodes(val);
30+
}
31+
return count;
32+
}
33+
autoinc () {
34+
console.time('autoinc')
35+
let val = this._maxInode(this._root.get("/")) + 1;
36+
console.timeEnd('autoinc')
37+
return val;
38+
}
39+
_maxInode(map) {
40+
let max = 0;
41+
for (let [key, val] of map) {
42+
if (key === STAT) continue;
43+
max = Math.max(max, val.get(STAT).ino);
44+
}
45+
return max;
46+
}
2147
print(root = this._root.get("/")) {
2248
let str = "";
2349
const printTree = (root, indent) => {
@@ -38,21 +64,17 @@ module.exports = class CacheFS {
3864
return str;
3965
}
4066
parse(print) {
67+
let autoinc = 0;
68+
4169
function mk(stat) {
70+
const ino = ++autoinc;
4271
// TODO: Use a better heuristic for determining whether file or dir
43-
if (stat.length === 1) {
44-
let [mode] = stat
45-
mode = parseInt(mode, 8);
46-
return new Map([
47-
[STAT, { mode, type: "dir", size: 0, mtimeMs: Date.now() }]
48-
]);
49-
} else {
50-
let [mode, size, mtimeMs] = stat;
51-
mode = parseInt(mode, 8);
52-
size = parseInt(size);
53-
mtimeMs = parseInt(mtimeMs);
54-
return new Map([[STAT, { mode, type: "file", size, mtimeMs }]]);
55-
}
72+
const type = stat.length === 1 ? "dir" : "file"
73+
let [mode, size, mtimeMs] = stat;
74+
mode = parseInt(mode, 8);
75+
size = size ? parseInt(size) : 0;
76+
mtimeMs = mtimeMs ? parseInt(mtimeMs) : Date.now();
77+
return new Map([[STAT, { mode, type, size, mtimeMs, ino }]]);
5678
}
5779

5880
let lines = print.trim().split("\n");
@@ -99,6 +121,7 @@ module.exports = class CacheFS {
99121
type: "dir",
100122
size: 0,
101123
mtimeMs: Date.now(),
124+
ino: this.autoinc(),
102125
};
103126
entry.set(STAT, stat);
104127
dir.set(basename, entry);
@@ -132,17 +155,29 @@ module.exports = class CacheFS {
132155
type: "file",
133156
size: data.length,
134157
mtimeMs: Date.now(),
158+
ino: this.autoinc(),
135159
};
136160
let entry = new Map();
137161
entry.set(STAT, stat);
138162
dir.set(basename, entry);
163+
return stat;
139164
}
140165
unlink(filepath) {
141166
// remove from parent
142167
let parent = this._lookup(path.dirname(filepath));
143168
let basename = path.basename(filepath);
144169
parent.delete(basename);
145170
}
171+
rename(oldFilepath, newFilepath) {
172+
// grab reference
173+
let entry = this._lookup(oldFilepath);
174+
// remove from parent directory
175+
this.unlink(oldFilepath)
176+
// insert into new parent directory
177+
let dir = this._lookup(path.dirname(newFilepath));
178+
let basename = path.basename(newFilepath);
179+
dir.set(basename, entry);
180+
}
146181
stat(filepath) {
147182
return this._lookup(filepath).get(STAT);
148183
}

src/IdbBackend.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ module.exports = class IdbBackend {
1111
loadSuperblock() {
1212
return idb.get("!root", this._store);
1313
}
14-
readFile(filepath) {
15-
return idb.get(filepath, this._store)
14+
readFile(inode) {
15+
return idb.get(inode, this._store)
1616
}
17-
writeFile(filepath, data) {
18-
return idb.set(filepath, data, this._store)
17+
writeFile(inode, data) {
18+
return idb.set(inode, data, this._store)
1919
}
20-
unlink(filepath) {
21-
return idb.del(filepath, this._store)
20+
unlink(inode) {
21+
return idb.del(inode, this._store)
2222
}
2323
wipe() {
2424
return idb.clear(this._store)

src/Stat.js

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,17 @@
1-
function SecondsNanoseconds(milliseconds) {
2-
if (milliseconds == null) {
3-
milliseconds = Date.now();
4-
}
5-
const seconds = Math.floor(milliseconds / 1000);
6-
const nanoseconds = (milliseconds - seconds * 1000) * 1000000;
7-
return [seconds, nanoseconds];
8-
}
9-
101
module.exports = class Stat {
112
constructor(stats) {
123
this.type = stats.type;
134
this.mode = stats.mode;
145
this.size = stats.size;
15-
const [mtimeSeconds, mtimeNanoseconds] = SecondsNanoseconds(stats.mtimeMs);
16-
const [ctimeSeconds, ctimeNanoseconds] = SecondsNanoseconds(stats.ctimeMs || stats.mtimeMs);
17-
this.mtimeSeconds = mtimeSeconds;
18-
this.ctimeSeconds = ctimeSeconds;
19-
this.mtimeNanoseconds = mtimeNanoseconds;
20-
this.ctimeNanoseconds = ctimeNanoseconds;
21-
6+
this.ino = stats.ino;
7+
this.mtimeMs = stats.mtimeMs;
8+
this.ctimeMs = stats.ctimeMs || stats.mtimeMs;
229
this.uid = 1;
2310
this.gid = 1;
2411
this.dev = 1;
25-
this.ino = 1;
12+
}
13+
isFile() {
14+
return this.type === "file";
2615
}
2716
isDirectory() {
2817
return this.type === "dir";

src/__tests__/CacheFS.spec.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,10 @@ describe("CacheFS module", () => {
1010
let text = fs.print(parsed)
1111
expect(text).toEqual(treeText)
1212
});
13+
it("size()", () => {
14+
expect(fs.size()).toEqual(0)
15+
fs.loadSuperBlock(treeText)
16+
let inodeCount = treeText.trim().split('\n').length
17+
expect(fs.size()).toEqual(inodeCount)
18+
});
1319
});

src/__tests__/fallback.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import FS from "../index.js";
22

3-
const fs = new FS("testfs", { wipe: true, url: 'http://localhost:9876/base/src/__tests__/__fixtures__/test-folder' });
3+
const fs = new FS("fallbackfs", { wipe: true, url: 'http://localhost:9876/base/src/__tests__/__fixtures__/test-folder' });
44

55
describe("http fallback", () => {
66
it("sanity check", () => {

src/__tests__/fs.spec.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,53 @@ describe("fs module", () => {
202202
});
203203
});
204204
});
205+
206+
describe("rename", () => {
207+
it("create and rename file", done => {
208+
fs.mkdir("/rename", () => {
209+
fs.writeFile("/rename/a.txt", "", () => {
210+
fs.rename("/rename/a.txt", "/rename/b.txt", (err) => {
211+
expect(err).toBe(null);
212+
fs.readdir("/rename", (err, data) => {
213+
expect(data.includes("a.txt")).toBe(false);
214+
expect(data.includes("b.txt")).toBe(true);
215+
fs.readFile("/rename/a.txt", (err, data) => {
216+
expect(err).not.toBe(null)
217+
expect(err.code).toBe("ENOENT")
218+
fs.readFile("/rename/b.txt", "utf8", (err, data) => {
219+
expect(err).toBe(null)
220+
expect(data).toBe("")
221+
done();
222+
});
223+
});
224+
});
225+
});
226+
});
227+
});
228+
});
229+
it("create and rename directory", done => {
230+
fs.mkdir("/rename", () => {
231+
fs.mkdir("/rename/a", () => {
232+
fs.writeFile("/rename/a/file.txt", "", () => {
233+
fs.rename("/rename/a", "/rename/b", (err) => {
234+
expect(err).toBe(null);
235+
fs.readdir("/rename", (err, data) => {
236+
expect(data.includes("a")).toBe(false);
237+
expect(data.includes("b")).toBe(true);
238+
fs.readFile("/rename/a/file.txt", (err, data) => {
239+
expect(err).not.toBe(null)
240+
expect(err.code).toBe("ENOENT")
241+
fs.readFile("/rename/b/file.txt", "utf8", (err, data) => {
242+
expect(err).toBe(null)
243+
expect(data).toBe("")
244+
done();
245+
});
246+
});
247+
});
248+
});
249+
});
250+
});
251+
});
252+
});
253+
});
205254
});

0 commit comments

Comments
 (0)