@zenfs/core 0.9.7 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backends/Index.d.ts +3 -3
- package/dist/backends/Index.js +23 -23
- package/dist/backends/backend.js +6 -5
- package/dist/backends/fetch.d.ts +84 -0
- package/dist/backends/fetch.js +170 -0
- package/dist/backends/{Locked.d.ts → locked.d.ts} +13 -13
- package/dist/backends/{Locked.js → locked.js} +54 -54
- package/dist/backends/{InMemory.d.ts → memory.d.ts} +7 -9
- package/dist/backends/memory.js +38 -0
- package/dist/backends/{Overlay.d.ts → overlay.d.ts} +12 -13
- package/dist/backends/{Overlay.js → overlay.js} +105 -110
- package/dist/backends/port/fs.d.ts +123 -0
- package/dist/backends/port/fs.js +239 -0
- package/dist/backends/port/rpc.d.ts +60 -0
- package/dist/backends/port/rpc.js +71 -0
- package/dist/backends/store/fs.d.ts +169 -0
- package/dist/backends/store/fs.js +743 -0
- package/dist/backends/store/simple.d.ts +64 -0
- package/dist/backends/store/simple.js +111 -0
- package/dist/backends/store/store.d.ts +111 -0
- package/dist/backends/store/store.js +62 -0
- package/dist/browser.min.js +4 -4
- package/dist/browser.min.js.map +4 -4
- package/dist/config.d.ts +8 -10
- package/dist/config.js +11 -11
- package/dist/emulation/async.js +6 -6
- package/dist/emulation/dir.js +2 -2
- package/dist/emulation/index.d.ts +1 -1
- package/dist/emulation/index.js +1 -1
- package/dist/emulation/path.d.ts +3 -2
- package/dist/emulation/path.js +19 -45
- package/dist/emulation/promises.d.ts +7 -12
- package/dist/emulation/promises.js +144 -146
- package/dist/emulation/shared.d.ts +5 -10
- package/dist/emulation/shared.js +9 -9
- package/dist/emulation/streams.js +3 -3
- package/dist/emulation/sync.js +25 -25
- package/dist/{ApiError.d.ts → error.d.ts} +13 -15
- package/dist/error.js +291 -0
- package/dist/file.d.ts +2 -0
- package/dist/file.js +11 -5
- package/dist/filesystem.d.ts +3 -3
- package/dist/filesystem.js +41 -44
- package/dist/index.d.ts +7 -6
- package/dist/index.js +7 -6
- package/dist/inode.d.ts +1 -1
- package/dist/mutex.js +2 -1
- package/dist/utils.d.ts +8 -7
- package/dist/utils.js +11 -12
- package/package.json +3 -3
- package/readme.md +17 -9
- package/src/backends/Index.ts +23 -23
- package/src/backends/backend.ts +8 -7
- package/src/backends/fetch.ts +229 -0
- package/src/backends/{Locked.ts → locked.ts} +55 -55
- package/src/backends/memory.ts +44 -0
- package/src/backends/{Overlay.ts → overlay.ts} +108 -114
- package/src/backends/port/fs.ts +306 -0
- package/src/backends/port/readme.md +59 -0
- package/src/backends/port/rpc.ts +144 -0
- package/src/backends/store/fs.ts +881 -0
- package/src/backends/store/readme.md +9 -0
- package/src/backends/store/simple.ts +144 -0
- package/src/backends/store/store.ts +164 -0
- package/src/config.ts +21 -25
- package/src/emulation/async.ts +6 -6
- package/src/emulation/dir.ts +2 -2
- package/src/emulation/index.ts +1 -1
- package/src/emulation/path.ts +25 -49
- package/src/emulation/promises.ts +150 -159
- package/src/emulation/shared.ts +13 -15
- package/src/emulation/streams.ts +3 -3
- package/src/emulation/sync.ts +28 -28
- package/src/{ApiError.ts → error.ts} +89 -90
- package/src/file.ts +13 -5
- package/src/filesystem.ts +44 -47
- package/src/index.ts +7 -6
- package/src/inode.ts +1 -1
- package/src/mutex.ts +3 -1
- package/src/utils.ts +16 -18
- package/tsconfig.json +2 -2
- package/dist/ApiError.js +0 -292
- package/dist/backends/AsyncStore.d.ts +0 -204
- package/dist/backends/AsyncStore.js +0 -509
- package/dist/backends/InMemory.js +0 -49
- package/dist/backends/SyncStore.d.ts +0 -213
- package/dist/backends/SyncStore.js +0 -445
- package/src/backends/AsyncStore.ts +0 -655
- package/src/backends/InMemory.ts +0 -56
- package/src/backends/SyncStore.ts +0 -589
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
import { W_OK, R_OK } from '../../emulation/constants.js';
|
|
2
|
+
import { dirname, basename, join, resolve } from '../../emulation/path.js';
|
|
3
|
+
import { ErrnoError, Errno } from '../../error.js';
|
|
4
|
+
import { PreloadFile, flagToMode } from '../../file.js';
|
|
5
|
+
import { FileSystem } from '../../filesystem.js';
|
|
6
|
+
import { Inode, rootIno, randomIno } from '../../inode.js';
|
|
7
|
+
import { FileType } from '../../stats.js';
|
|
8
|
+
import { encodeDirListing, encode, decodeDirListing } from '../../utils.js';
|
|
9
|
+
const maxInodeAllocTries = 5;
|
|
10
|
+
/**
|
|
11
|
+
* A synchronous key-value file system. Uses a SyncStore to store the data.
|
|
12
|
+
*
|
|
13
|
+
* We use a unique ID for each node in the file system. The root node has a fixed ID.
|
|
14
|
+
* @todo Introduce Node ID caching.
|
|
15
|
+
* @todo Check modes.
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export class StoreFS extends FileSystem {
|
|
19
|
+
get store() {
|
|
20
|
+
if (!this._store) {
|
|
21
|
+
throw new ErrnoError(Errno.ENODATA, 'No store attached');
|
|
22
|
+
}
|
|
23
|
+
return this._store;
|
|
24
|
+
}
|
|
25
|
+
async ready() {
|
|
26
|
+
await super.ready();
|
|
27
|
+
if (this._initialized) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
this._initialized = true;
|
|
31
|
+
this._store = await this.$store;
|
|
32
|
+
}
|
|
33
|
+
constructor($store) {
|
|
34
|
+
super();
|
|
35
|
+
this.$store = $store;
|
|
36
|
+
this._initialized = false;
|
|
37
|
+
if (!($store instanceof Promise)) {
|
|
38
|
+
this._store = $store;
|
|
39
|
+
this._initialized = true;
|
|
40
|
+
this.makeRootDirectorySync();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
metadata() {
|
|
44
|
+
return {
|
|
45
|
+
...super.metadata(),
|
|
46
|
+
name: this.store.name,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Delete all contents stored in the file system.
|
|
51
|
+
*/
|
|
52
|
+
async empty() {
|
|
53
|
+
await this.store.clear();
|
|
54
|
+
// Root always exists.
|
|
55
|
+
await this.makeRootDirectory();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Delete all contents stored in the file system.
|
|
59
|
+
*/
|
|
60
|
+
emptySync() {
|
|
61
|
+
this.store.clearSync();
|
|
62
|
+
// Root always exists.
|
|
63
|
+
this.makeRootDirectorySync();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* @todo Make rename compatible with the cache.
|
|
67
|
+
*/
|
|
68
|
+
async rename(oldPath, newPath, cred) {
|
|
69
|
+
const tx = this.store.transaction(), oldParent = dirname(oldPath), oldName = basename(oldPath), newParent = dirname(newPath), newName = basename(newPath),
|
|
70
|
+
// Remove oldPath from parent's directory listing.
|
|
71
|
+
oldDirNode = await this.findINode(tx, oldParent), oldDirList = await this.getDirListing(tx, oldDirNode, oldParent);
|
|
72
|
+
if (!oldDirNode.toStats().hasAccess(W_OK, cred)) {
|
|
73
|
+
throw ErrnoError.With('EACCES', oldPath, 'rename');
|
|
74
|
+
}
|
|
75
|
+
if (!oldDirList[oldName]) {
|
|
76
|
+
throw ErrnoError.With('ENOENT', oldPath, 'rename');
|
|
77
|
+
}
|
|
78
|
+
const nodeId = oldDirList[oldName];
|
|
79
|
+
delete oldDirList[oldName];
|
|
80
|
+
// Invariant: Can't move a folder inside itself.
|
|
81
|
+
// This funny little hack ensures that the check passes only if oldPath
|
|
82
|
+
// is a subpath of newParent. We append '/' to avoid matching folders that
|
|
83
|
+
// are a substring of the bottom-most folder in the path.
|
|
84
|
+
if ((newParent + '/').indexOf(oldPath + '/') === 0) {
|
|
85
|
+
throw new ErrnoError(Errno.EBUSY, oldParent);
|
|
86
|
+
}
|
|
87
|
+
// Add newPath to parent's directory listing.
|
|
88
|
+
let newDirNode, newDirList;
|
|
89
|
+
if (newParent === oldParent) {
|
|
90
|
+
// Prevent us from re-grabbing the same directory listing, which still
|
|
91
|
+
// contains oldName.
|
|
92
|
+
newDirNode = oldDirNode;
|
|
93
|
+
newDirList = oldDirList;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
newDirNode = await this.findINode(tx, newParent);
|
|
97
|
+
newDirList = await this.getDirListing(tx, newDirNode, newParent);
|
|
98
|
+
}
|
|
99
|
+
if (newDirList[newName]) {
|
|
100
|
+
// If it's a file, delete it.
|
|
101
|
+
const newNameNode = await this.getINode(tx, newDirList[newName], newPath);
|
|
102
|
+
if (newNameNode.toStats().isFile()) {
|
|
103
|
+
try {
|
|
104
|
+
await tx.remove(newNameNode.ino);
|
|
105
|
+
await tx.remove(newDirList[newName]);
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
await tx.abort();
|
|
109
|
+
throw e;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// If it's a directory, throw a permissions error.
|
|
114
|
+
throw ErrnoError.With('EPERM', newPath, 'rename');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
newDirList[newName] = nodeId;
|
|
118
|
+
// Commit the two changed directory listings.
|
|
119
|
+
try {
|
|
120
|
+
await tx.set(oldDirNode.ino, encodeDirListing(oldDirList));
|
|
121
|
+
await tx.set(newDirNode.ino, encodeDirListing(newDirList));
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
await tx.abort();
|
|
125
|
+
throw e;
|
|
126
|
+
}
|
|
127
|
+
await tx.commit();
|
|
128
|
+
}
|
|
129
|
+
renameSync(oldPath, newPath, cred) {
|
|
130
|
+
const tx = this.store.transaction(), oldParent = dirname(oldPath), oldName = basename(oldPath), newParent = dirname(newPath), newName = basename(newPath),
|
|
131
|
+
// Remove oldPath from parent's directory listing.
|
|
132
|
+
oldDirNode = this.findINodeSync(tx, oldParent), oldDirList = this.getDirListingSync(tx, oldDirNode, oldParent);
|
|
133
|
+
if (!oldDirNode.toStats().hasAccess(W_OK, cred)) {
|
|
134
|
+
throw ErrnoError.With('EACCES', oldPath, 'rename');
|
|
135
|
+
}
|
|
136
|
+
if (!oldDirList[oldName]) {
|
|
137
|
+
throw ErrnoError.With('ENOENT', oldPath, 'rename');
|
|
138
|
+
}
|
|
139
|
+
const ino = oldDirList[oldName];
|
|
140
|
+
delete oldDirList[oldName];
|
|
141
|
+
// Invariant: Can't move a folder inside itself.
|
|
142
|
+
// This funny little hack ensures that the check passes only if oldPath
|
|
143
|
+
// is a subpath of newParent. We append '/' to avoid matching folders that
|
|
144
|
+
// are a substring of the bottom-most folder in the path.
|
|
145
|
+
if ((newParent + '/').indexOf(oldPath + '/') == 0) {
|
|
146
|
+
throw new ErrnoError(Errno.EBUSY, oldParent);
|
|
147
|
+
}
|
|
148
|
+
// Add newPath to parent's directory listing.
|
|
149
|
+
let newDirNode, newDirList;
|
|
150
|
+
if (newParent === oldParent) {
|
|
151
|
+
// Prevent us from re-grabbing the same directory listing, which still
|
|
152
|
+
// contains oldName.
|
|
153
|
+
newDirNode = oldDirNode;
|
|
154
|
+
newDirList = oldDirList;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
newDirNode = this.findINodeSync(tx, newParent);
|
|
158
|
+
newDirList = this.getDirListingSync(tx, newDirNode, newParent);
|
|
159
|
+
}
|
|
160
|
+
if (newDirList[newName]) {
|
|
161
|
+
// If it's a file, delete it.
|
|
162
|
+
const newNameNode = this.getINodeSync(tx, newDirList[newName], newPath);
|
|
163
|
+
if (newNameNode.toStats().isFile()) {
|
|
164
|
+
try {
|
|
165
|
+
tx.removeSync(newNameNode.ino);
|
|
166
|
+
tx.removeSync(newDirList[newName]);
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
tx.abortSync();
|
|
170
|
+
throw e;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
// If it's a directory, throw a permissions error.
|
|
175
|
+
throw ErrnoError.With('EPERM', newPath, 'rename');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
newDirList[newName] = ino;
|
|
179
|
+
// Commit the two changed directory listings.
|
|
180
|
+
try {
|
|
181
|
+
tx.setSync(oldDirNode.ino, encodeDirListing(oldDirList));
|
|
182
|
+
tx.setSync(newDirNode.ino, encodeDirListing(newDirList));
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
tx.abortSync();
|
|
186
|
+
throw e;
|
|
187
|
+
}
|
|
188
|
+
tx.commitSync();
|
|
189
|
+
}
|
|
190
|
+
async stat(path, cred) {
|
|
191
|
+
const tx = this.store.transaction();
|
|
192
|
+
const inode = await this.findINode(tx, path);
|
|
193
|
+
if (!inode) {
|
|
194
|
+
throw ErrnoError.With('ENOENT', path, 'stat');
|
|
195
|
+
}
|
|
196
|
+
const stats = inode.toStats();
|
|
197
|
+
if (!stats.hasAccess(R_OK, cred)) {
|
|
198
|
+
throw ErrnoError.With('EACCES', path, 'stat');
|
|
199
|
+
}
|
|
200
|
+
return stats;
|
|
201
|
+
}
|
|
202
|
+
statSync(path, cred) {
|
|
203
|
+
// Get the inode to the item, convert it into a Stats object.
|
|
204
|
+
const stats = this.findINodeSync(this.store.transaction(), path).toStats();
|
|
205
|
+
if (!stats.hasAccess(R_OK, cred)) {
|
|
206
|
+
throw ErrnoError.With('EACCES', path, 'stat');
|
|
207
|
+
}
|
|
208
|
+
return stats;
|
|
209
|
+
}
|
|
210
|
+
async createFile(path, flag, mode, cred) {
|
|
211
|
+
const data = new Uint8Array(0);
|
|
212
|
+
const file = await this.commitNew(this.store.transaction(), path, FileType.FILE, mode, cred, data);
|
|
213
|
+
return new PreloadFile(this, path, flag, file.toStats(), data);
|
|
214
|
+
}
|
|
215
|
+
createFileSync(path, flag, mode, cred) {
|
|
216
|
+
this.commitNewSync(path, FileType.FILE, mode, cred);
|
|
217
|
+
return this.openFileSync(path, flag, cred);
|
|
218
|
+
}
|
|
219
|
+
async openFile(path, flag, cred) {
|
|
220
|
+
const tx = this.store.transaction(), node = await this.findINode(tx, path), data = await tx.get(node.ino);
|
|
221
|
+
if (!node.toStats().hasAccess(flagToMode(flag), cred)) {
|
|
222
|
+
throw ErrnoError.With('EACCES', path, 'openFile');
|
|
223
|
+
}
|
|
224
|
+
if (!data) {
|
|
225
|
+
throw ErrnoError.With('ENOENT', path, 'openFile');
|
|
226
|
+
}
|
|
227
|
+
return new PreloadFile(this, path, flag, node.toStats(), data);
|
|
228
|
+
}
|
|
229
|
+
openFileSync(path, flag, cred) {
|
|
230
|
+
const tx = this.store.transaction(), node = this.findINodeSync(tx, path), data = tx.getSync(node.ino);
|
|
231
|
+
if (!node.toStats().hasAccess(flagToMode(flag), cred)) {
|
|
232
|
+
throw ErrnoError.With('EACCES', path, 'openFile');
|
|
233
|
+
}
|
|
234
|
+
if (!data) {
|
|
235
|
+
throw ErrnoError.With('ENOENT', path, 'openFile');
|
|
236
|
+
}
|
|
237
|
+
return new PreloadFile(this, path, flag, node.toStats(), data);
|
|
238
|
+
}
|
|
239
|
+
async unlink(path, cred) {
|
|
240
|
+
return this.remove(path, false, cred);
|
|
241
|
+
}
|
|
242
|
+
unlinkSync(path, cred) {
|
|
243
|
+
this.removeSync(path, false, cred);
|
|
244
|
+
}
|
|
245
|
+
async rmdir(path, cred) {
|
|
246
|
+
// Check first if directory is empty.
|
|
247
|
+
const list = await this.readdir(path, cred);
|
|
248
|
+
if (list.length > 0) {
|
|
249
|
+
throw ErrnoError.With('ENOTEMPTY', path, 'rmdir');
|
|
250
|
+
}
|
|
251
|
+
await this.remove(path, true, cred);
|
|
252
|
+
}
|
|
253
|
+
rmdirSync(path, cred) {
|
|
254
|
+
// Check first if directory is empty.
|
|
255
|
+
if (this.readdirSync(path, cred).length > 0) {
|
|
256
|
+
throw ErrnoError.With('ENOTEMPTY', path, 'rmdir');
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
this.removeSync(path, true, cred);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async mkdir(path, mode, cred) {
|
|
263
|
+
const tx = this.store.transaction(), data = encode('{}');
|
|
264
|
+
await this.commitNew(tx, path, FileType.DIRECTORY, mode, cred, data);
|
|
265
|
+
}
|
|
266
|
+
mkdirSync(path, mode, cred) {
|
|
267
|
+
this.commitNewSync(path, FileType.DIRECTORY, mode, cred, encode('{}'));
|
|
268
|
+
}
|
|
269
|
+
async readdir(path, cred) {
|
|
270
|
+
const tx = this.store.transaction();
|
|
271
|
+
const node = await this.findINode(tx, path);
|
|
272
|
+
if (!node.toStats().hasAccess(R_OK, cred)) {
|
|
273
|
+
throw ErrnoError.With('EACCES', path, 'readdur');
|
|
274
|
+
}
|
|
275
|
+
return Object.keys(await this.getDirListing(tx, node, path));
|
|
276
|
+
}
|
|
277
|
+
readdirSync(path, cred) {
|
|
278
|
+
const tx = this.store.transaction();
|
|
279
|
+
const node = this.findINodeSync(tx, path);
|
|
280
|
+
if (!node.toStats().hasAccess(R_OK, cred)) {
|
|
281
|
+
throw ErrnoError.With('EACCES', path, 'readdir');
|
|
282
|
+
}
|
|
283
|
+
return Object.keys(this.getDirListingSync(tx, node, path));
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Updated the inode and data node at the given path
|
|
287
|
+
* @todo Ensure mtime updates properly, and use that to determine if a data update is required.
|
|
288
|
+
*/
|
|
289
|
+
async sync(path, data, stats) {
|
|
290
|
+
const tx = this.store.transaction(),
|
|
291
|
+
// We use _findInode because we actually need the INode id.
|
|
292
|
+
fileInodeId = await this._findINode(tx, dirname(path), basename(path)), fileInode = await this.getINode(tx, fileInodeId, path), inodeChanged = fileInode.update(stats);
|
|
293
|
+
try {
|
|
294
|
+
// Sync data.
|
|
295
|
+
await tx.set(fileInode.ino, data);
|
|
296
|
+
// Sync metadata.
|
|
297
|
+
if (inodeChanged) {
|
|
298
|
+
await tx.set(fileInodeId, fileInode.data);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
await tx.abort();
|
|
303
|
+
throw e;
|
|
304
|
+
}
|
|
305
|
+
await tx.commit();
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Updated the inode and data node at the given path
|
|
309
|
+
* @todo Ensure mtime updates properly, and use that to determine if a data update is required.
|
|
310
|
+
*/
|
|
311
|
+
syncSync(path, data, stats) {
|
|
312
|
+
const tx = this.store.transaction(),
|
|
313
|
+
// We use _findInode because we actually need the INode id.
|
|
314
|
+
fileInodeId = this._findINodeSync(tx, dirname(path), basename(path)), fileInode = this.getINodeSync(tx, fileInodeId, path), inodeChanged = fileInode.update(stats);
|
|
315
|
+
try {
|
|
316
|
+
// Sync data.
|
|
317
|
+
tx.setSync(fileInode.ino, data);
|
|
318
|
+
// Sync metadata.
|
|
319
|
+
if (inodeChanged) {
|
|
320
|
+
tx.setSync(fileInodeId, fileInode.data);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch (e) {
|
|
324
|
+
tx.abortSync();
|
|
325
|
+
throw e;
|
|
326
|
+
}
|
|
327
|
+
tx.commitSync();
|
|
328
|
+
}
|
|
329
|
+
async link(existing, newpath, cred) {
|
|
330
|
+
const tx = this.store.transaction(), existingDir = dirname(existing), existingDirNode = await this.findINode(tx, existingDir);
|
|
331
|
+
if (!existingDirNode.toStats().hasAccess(R_OK, cred)) {
|
|
332
|
+
throw ErrnoError.With('EACCES', existingDir, 'link');
|
|
333
|
+
}
|
|
334
|
+
const newDir = dirname(newpath), newDirNode = await this.findINode(tx, newDir), newListing = await this.getDirListing(tx, newDirNode, newDir);
|
|
335
|
+
if (!newDirNode.toStats().hasAccess(W_OK, cred)) {
|
|
336
|
+
throw ErrnoError.With('EACCES', newDir, 'link');
|
|
337
|
+
}
|
|
338
|
+
const ino = await this._findINode(tx, existingDir, basename(existing));
|
|
339
|
+
const node = await this.getINode(tx, ino, existing);
|
|
340
|
+
if (!node.toStats().hasAccess(W_OK, cred)) {
|
|
341
|
+
throw ErrnoError.With('EACCES', newpath, 'link');
|
|
342
|
+
}
|
|
343
|
+
node.nlink++;
|
|
344
|
+
newListing[basename(newpath)] = ino;
|
|
345
|
+
try {
|
|
346
|
+
tx.setSync(ino, node.data);
|
|
347
|
+
tx.setSync(newDirNode.ino, encodeDirListing(newListing));
|
|
348
|
+
}
|
|
349
|
+
catch (e) {
|
|
350
|
+
tx.abortSync();
|
|
351
|
+
throw e;
|
|
352
|
+
}
|
|
353
|
+
tx.commitSync();
|
|
354
|
+
}
|
|
355
|
+
linkSync(existing, newpath, cred) {
|
|
356
|
+
const tx = this.store.transaction(), existingDir = dirname(existing), existingDirNode = this.findINodeSync(tx, existingDir);
|
|
357
|
+
if (!existingDirNode.toStats().hasAccess(R_OK, cred)) {
|
|
358
|
+
throw ErrnoError.With('EACCES', existingDir, 'link');
|
|
359
|
+
}
|
|
360
|
+
const newDir = dirname(newpath), newDirNode = this.findINodeSync(tx, newDir), newListing = this.getDirListingSync(tx, newDirNode, newDir);
|
|
361
|
+
if (!newDirNode.toStats().hasAccess(W_OK, cred)) {
|
|
362
|
+
throw ErrnoError.With('EACCES', newDir, 'link');
|
|
363
|
+
}
|
|
364
|
+
const ino = this._findINodeSync(tx, existingDir, basename(existing));
|
|
365
|
+
const node = this.getINodeSync(tx, ino, existing);
|
|
366
|
+
if (!node.toStats().hasAccess(W_OK, cred)) {
|
|
367
|
+
throw ErrnoError.With('EACCES', newpath, 'link');
|
|
368
|
+
}
|
|
369
|
+
node.nlink++;
|
|
370
|
+
newListing[basename(newpath)] = ino;
|
|
371
|
+
try {
|
|
372
|
+
tx.setSync(ino, node.data);
|
|
373
|
+
tx.setSync(newDirNode.ino, encodeDirListing(newListing));
|
|
374
|
+
}
|
|
375
|
+
catch (e) {
|
|
376
|
+
tx.abortSync();
|
|
377
|
+
throw e;
|
|
378
|
+
}
|
|
379
|
+
tx.commitSync();
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Checks if the root directory exists. Creates it if it doesn't.
|
|
383
|
+
*/
|
|
384
|
+
async makeRootDirectory() {
|
|
385
|
+
const tx = this.store.transaction();
|
|
386
|
+
if (await tx.get(rootIno)) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
// Create new inode. o777, owned by root:root
|
|
390
|
+
const inode = new Inode();
|
|
391
|
+
inode.mode = 0o777 | FileType.DIRECTORY;
|
|
392
|
+
// If the root doesn't exist, the first random ID shouldn't exist either.
|
|
393
|
+
await tx.set(inode.ino, encode('{}'));
|
|
394
|
+
await tx.set(rootIno, inode.data);
|
|
395
|
+
await tx.commit();
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Checks if the root directory exists. Creates it if it doesn't.
|
|
399
|
+
*/
|
|
400
|
+
makeRootDirectorySync() {
|
|
401
|
+
const tx = this.store.transaction();
|
|
402
|
+
if (tx.getSync(rootIno)) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
// Create new inode, mode o777, owned by root:root
|
|
406
|
+
const inode = new Inode();
|
|
407
|
+
inode.mode = 0o777 | FileType.DIRECTORY;
|
|
408
|
+
// If the root doesn't exist, the first random ID shouldn't exist either.
|
|
409
|
+
tx.setSync(inode.ino, encode('{}'));
|
|
410
|
+
tx.setSync(rootIno, inode.data);
|
|
411
|
+
tx.commitSync();
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Helper function for findINode.
|
|
415
|
+
* @param parent The parent directory of the file we are attempting to find.
|
|
416
|
+
* @param filename The filename of the inode we are attempting to find, minus
|
|
417
|
+
* the parent.
|
|
418
|
+
*/
|
|
419
|
+
async _findINode(tx, parent, filename, visited = new Set()) {
|
|
420
|
+
const currentPath = join(parent, filename);
|
|
421
|
+
if (visited.has(currentPath)) {
|
|
422
|
+
throw new ErrnoError(Errno.EIO, 'Infinite loop detected while finding inode', currentPath);
|
|
423
|
+
}
|
|
424
|
+
visited.add(currentPath);
|
|
425
|
+
if (parent == '/' && filename === '') {
|
|
426
|
+
return rootIno;
|
|
427
|
+
}
|
|
428
|
+
const inode = parent == '/' ? await this.getINode(tx, rootIno, parent) : await this.findINode(tx, parent, visited);
|
|
429
|
+
const dirList = await this.getDirListing(tx, inode, parent);
|
|
430
|
+
if (!(filename in dirList)) {
|
|
431
|
+
throw ErrnoError.With('ENOENT', resolve(parent, filename), '_findINode');
|
|
432
|
+
}
|
|
433
|
+
return dirList[filename];
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Helper function for findINode.
|
|
437
|
+
* @param parent The parent directory of the file we are attempting to find.
|
|
438
|
+
* @param filename The filename of the inode we are attempting to find, minus
|
|
439
|
+
* the parent.
|
|
440
|
+
* @return string The ID of the file's inode in the file system.
|
|
441
|
+
*/
|
|
442
|
+
_findINodeSync(tx, parent, filename, visited = new Set()) {
|
|
443
|
+
const currentPath = join(parent, filename);
|
|
444
|
+
if (visited.has(currentPath)) {
|
|
445
|
+
throw new ErrnoError(Errno.EIO, 'Infinite loop detected while finding inode', currentPath);
|
|
446
|
+
}
|
|
447
|
+
visited.add(currentPath);
|
|
448
|
+
if (parent == '/' && filename === '') {
|
|
449
|
+
return rootIno;
|
|
450
|
+
}
|
|
451
|
+
const inode = parent == '/' ? this.getINodeSync(tx, rootIno, parent) : this.findINodeSync(tx, parent, visited);
|
|
452
|
+
const dir = this.getDirListingSync(tx, inode, parent);
|
|
453
|
+
if (!(filename in dir)) {
|
|
454
|
+
throw ErrnoError.With('ENOENT', resolve(parent, filename), '_findINode');
|
|
455
|
+
}
|
|
456
|
+
return dir[filename];
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Finds the Inode of the given path.
|
|
460
|
+
* @param path The path to look up.
|
|
461
|
+
* @todo memoize/cache
|
|
462
|
+
*/
|
|
463
|
+
async findINode(tx, path, visited = new Set()) {
|
|
464
|
+
const id = await this._findINode(tx, dirname(path), basename(path), visited);
|
|
465
|
+
return this.getINode(tx, id, path);
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Finds the Inode of the given path.
|
|
469
|
+
* @param path The path to look up.
|
|
470
|
+
* @return The Inode of the path p.
|
|
471
|
+
* @todo memoize/cache
|
|
472
|
+
*/
|
|
473
|
+
findINodeSync(tx, path, visited = new Set()) {
|
|
474
|
+
const ino = this._findINodeSync(tx, dirname(path), basename(path), visited);
|
|
475
|
+
return this.getINodeSync(tx, ino, path);
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Given the ID of a node, retrieves the corresponding Inode.
|
|
479
|
+
* @param tx The transaction to use.
|
|
480
|
+
* @param path The corresponding path to the file (used for error messages).
|
|
481
|
+
* @param id The ID to look up.
|
|
482
|
+
*/
|
|
483
|
+
async getINode(tx, id, path) {
|
|
484
|
+
const data = await tx.get(id);
|
|
485
|
+
if (!data) {
|
|
486
|
+
throw ErrnoError.With('ENOENT', path, 'getINode');
|
|
487
|
+
}
|
|
488
|
+
return new Inode(data.buffer);
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Given the ID of a node, retrieves the corresponding Inode.
|
|
492
|
+
* @param tx The transaction to use.
|
|
493
|
+
* @param path The corresponding path to the file (used for error messages).
|
|
494
|
+
* @param id The ID to look up.
|
|
495
|
+
*/
|
|
496
|
+
getINodeSync(tx, id, path) {
|
|
497
|
+
const data = tx.getSync(id);
|
|
498
|
+
if (!data) {
|
|
499
|
+
throw ErrnoError.With('ENOENT', path, 'getINode');
|
|
500
|
+
}
|
|
501
|
+
const inode = new Inode(data.buffer);
|
|
502
|
+
return inode;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Given the Inode of a directory, retrieves the corresponding directory
|
|
506
|
+
* listing.
|
|
507
|
+
*/
|
|
508
|
+
async getDirListing(tx, inode, path) {
|
|
509
|
+
if (!inode.toStats().isDirectory()) {
|
|
510
|
+
throw ErrnoError.With('ENOTDIR', path, 'getDirListing');
|
|
511
|
+
}
|
|
512
|
+
const data = await tx.get(inode.ino);
|
|
513
|
+
if (!data) {
|
|
514
|
+
/*
|
|
515
|
+
Occurs when data is undefined, or corresponds to something other
|
|
516
|
+
than a directory listing. The latter should never occur unless
|
|
517
|
+
the file system is corrupted.
|
|
518
|
+
*/
|
|
519
|
+
throw ErrnoError.With('ENOENT', path, 'getDirListing');
|
|
520
|
+
}
|
|
521
|
+
return decodeDirListing(data);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Given the Inode of a directory, retrieves the corresponding directory listing.
|
|
525
|
+
*/
|
|
526
|
+
getDirListingSync(tx, inode, p) {
|
|
527
|
+
if (!inode.toStats().isDirectory()) {
|
|
528
|
+
throw ErrnoError.With('ENOTDIR', p, 'getDirListing');
|
|
529
|
+
}
|
|
530
|
+
const data = tx.getSync(inode.ino);
|
|
531
|
+
if (!data) {
|
|
532
|
+
throw ErrnoError.With('ENOENT', p, 'getDirListing');
|
|
533
|
+
}
|
|
534
|
+
return decodeDirListing(data);
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Adds a new node under a random ID. Retries before giving up in
|
|
538
|
+
* the exceedingly unlikely chance that we try to reuse a random ino.
|
|
539
|
+
*/
|
|
540
|
+
async addNew(tx, data, path) {
|
|
541
|
+
for (let i = 0; i < maxInodeAllocTries; i++) {
|
|
542
|
+
const ino = randomIno();
|
|
543
|
+
if (await tx.get(ino)) {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
await tx.set(ino, data);
|
|
547
|
+
return ino;
|
|
548
|
+
}
|
|
549
|
+
throw new ErrnoError(Errno.ENOSPC, 'No inode IDs available', path, 'addNewNode');
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Creates a new node under a random ID. Retries before giving up in
|
|
553
|
+
* the exceedingly unlikely chance that we try to reuse a random ino.
|
|
554
|
+
* @return The ino that the data was stored under.
|
|
555
|
+
*/
|
|
556
|
+
addNewSync(tx, data, path) {
|
|
557
|
+
for (let i = 0; i < maxInodeAllocTries; i++) {
|
|
558
|
+
const ino = randomIno();
|
|
559
|
+
if (tx.getSync(ino)) {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
tx.setSync(ino, data);
|
|
563
|
+
return ino;
|
|
564
|
+
}
|
|
565
|
+
throw new ErrnoError(Errno.ENOSPC, 'No inode IDs available', path, 'addNewNode');
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Commits a new file (well, a FILE or a DIRECTORY) to the file system with
|
|
569
|
+
* the given mode.
|
|
570
|
+
* Note: This will commit the transaction.
|
|
571
|
+
* @param path The path to the new file.
|
|
572
|
+
* @param type The type of the new file.
|
|
573
|
+
* @param mode The mode to create the new file with.
|
|
574
|
+
* @param cred The UID/GID to create the file with
|
|
575
|
+
* @param data The data to store at the file's data node.
|
|
576
|
+
*/
|
|
577
|
+
async commitNew(tx, path, type, mode, cred, data) {
|
|
578
|
+
const parentPath = dirname(path), parent = await this.findINode(tx, parentPath);
|
|
579
|
+
//Check that the creater has correct access
|
|
580
|
+
if (!parent.toStats().hasAccess(W_OK, cred)) {
|
|
581
|
+
throw ErrnoError.With('EACCES', path, 'commitNewFile');
|
|
582
|
+
}
|
|
583
|
+
const fname = basename(path), listing = await this.getDirListing(tx, parent, parentPath);
|
|
584
|
+
/*
|
|
585
|
+
The root always exists.
|
|
586
|
+
If we don't check this prior to taking steps below,
|
|
587
|
+
we will create a file with name '' in root should path == '/'.
|
|
588
|
+
*/
|
|
589
|
+
if (path === '/') {
|
|
590
|
+
throw ErrnoError.With('EEXIST', path, 'commitNewFile');
|
|
591
|
+
}
|
|
592
|
+
// Check if file already exists.
|
|
593
|
+
if (listing[fname]) {
|
|
594
|
+
await tx.abort();
|
|
595
|
+
throw ErrnoError.With('EEXIST', path, 'commitNewFile');
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
// Commit data.
|
|
599
|
+
const inode = new Inode();
|
|
600
|
+
inode.ino = await this.addNew(tx, data, path);
|
|
601
|
+
inode.mode = mode | type;
|
|
602
|
+
inode.uid = cred.uid;
|
|
603
|
+
inode.gid = cred.gid;
|
|
604
|
+
inode.size = data.length;
|
|
605
|
+
// Update and commit parent directory listing.
|
|
606
|
+
listing[fname] = await this.addNew(tx, inode.data, path);
|
|
607
|
+
await tx.set(parent.ino, encodeDirListing(listing));
|
|
608
|
+
await tx.commit();
|
|
609
|
+
return inode;
|
|
610
|
+
}
|
|
611
|
+
catch (e) {
|
|
612
|
+
tx.abort();
|
|
613
|
+
throw e;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Commits a new file (well, a FILE or a DIRECTORY) to the file system with the given mode.
|
|
618
|
+
* Note: This will commit the transaction.
|
|
619
|
+
* @param path The path to the new file.
|
|
620
|
+
* @param type The type of the new file.
|
|
621
|
+
* @param mode The mode to create the new file with.
|
|
622
|
+
* @param data The data to store at the file's data node.
|
|
623
|
+
* @return The Inode for the new file.
|
|
624
|
+
*/
|
|
625
|
+
commitNewSync(path, type, mode, cred, data = new Uint8Array()) {
|
|
626
|
+
const tx = this.store.transaction(), parentPath = dirname(path), parent = this.findINodeSync(tx, parentPath);
|
|
627
|
+
//Check that the creater has correct access
|
|
628
|
+
if (!parent.toStats().hasAccess(W_OK, cred)) {
|
|
629
|
+
throw ErrnoError.With('EACCES', path, 'commitNewFile');
|
|
630
|
+
}
|
|
631
|
+
const fname = basename(path), listing = this.getDirListingSync(tx, parent, parentPath);
|
|
632
|
+
/*
|
|
633
|
+
The root always exists.
|
|
634
|
+
If we don't check this prior to taking steps below,
|
|
635
|
+
we will create a file with name '' in root should p == '/'.
|
|
636
|
+
*/
|
|
637
|
+
if (path === '/') {
|
|
638
|
+
throw ErrnoError.With('EEXIST', path, 'commitNewFile');
|
|
639
|
+
}
|
|
640
|
+
// Check if file already exists.
|
|
641
|
+
if (listing[fname]) {
|
|
642
|
+
throw ErrnoError.With('EEXIST', path, 'commitNewFile');
|
|
643
|
+
}
|
|
644
|
+
const node = new Inode();
|
|
645
|
+
try {
|
|
646
|
+
// Commit data.
|
|
647
|
+
node.ino = this.addNewSync(tx, data, path);
|
|
648
|
+
node.size = data.length;
|
|
649
|
+
node.mode = mode | type;
|
|
650
|
+
node.uid = cred.uid;
|
|
651
|
+
node.gid = cred.gid;
|
|
652
|
+
// Update and commit parent directory listing.
|
|
653
|
+
listing[fname] = this.addNewSync(tx, node.data, path);
|
|
654
|
+
tx.setSync(parent.ino, encodeDirListing(listing));
|
|
655
|
+
}
|
|
656
|
+
catch (e) {
|
|
657
|
+
tx.abortSync();
|
|
658
|
+
throw e;
|
|
659
|
+
}
|
|
660
|
+
tx.commitSync();
|
|
661
|
+
return node;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Remove all traces of the given path from the file system.
|
|
665
|
+
* @param path The path to remove from the file system.
|
|
666
|
+
* @param isDir Does the path belong to a directory, or a file?
|
|
667
|
+
* @todo Update mtime.
|
|
668
|
+
*/
|
|
669
|
+
async remove(path, isDir, cred) {
|
|
670
|
+
const tx = this.store.transaction(), parent = dirname(path), parentNode = await this.findINode(tx, parent), listing = await this.getDirListing(tx, parentNode, parent), fileName = basename(path);
|
|
671
|
+
if (!listing[fileName]) {
|
|
672
|
+
throw ErrnoError.With('ENOENT', path, 'removeEntry');
|
|
673
|
+
}
|
|
674
|
+
const fileIno = listing[fileName];
|
|
675
|
+
// Get file inode.
|
|
676
|
+
const fileNode = await this.getINode(tx, fileIno, path);
|
|
677
|
+
if (!fileNode.toStats().hasAccess(W_OK, cred)) {
|
|
678
|
+
throw ErrnoError.With('EACCES', path, 'removeEntry');
|
|
679
|
+
}
|
|
680
|
+
// Remove from directory listing of parent.
|
|
681
|
+
delete listing[fileName];
|
|
682
|
+
if (!isDir && fileNode.toStats().isDirectory()) {
|
|
683
|
+
throw ErrnoError.With('EISDIR', path, 'removeEntry');
|
|
684
|
+
}
|
|
685
|
+
if (isDir && !fileNode.toStats().isDirectory()) {
|
|
686
|
+
throw ErrnoError.With('ENOTDIR', path, 'removeEntry');
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
await tx.set(parentNode.ino, encodeDirListing(listing));
|
|
690
|
+
if (--fileNode.nlink < 1) {
|
|
691
|
+
// remove file
|
|
692
|
+
await tx.remove(fileNode.ino);
|
|
693
|
+
await tx.remove(fileIno);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
catch (e) {
|
|
697
|
+
await tx.abort();
|
|
698
|
+
throw e;
|
|
699
|
+
}
|
|
700
|
+
// Success.
|
|
701
|
+
await tx.commit();
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Remove all traces of the given path from the file system.
|
|
705
|
+
* @param path The path to remove from the file system.
|
|
706
|
+
* @param isDir Does the path belong to a directory, or a file?
|
|
707
|
+
* @todo Update mtime.
|
|
708
|
+
*/
|
|
709
|
+
removeSync(path, isDir, cred) {
|
|
710
|
+
const tx = this.store.transaction(), parent = dirname(path), parentNode = this.findINodeSync(tx, parent), listing = this.getDirListingSync(tx, parentNode, parent), fileName = basename(path), fileIno = listing[fileName];
|
|
711
|
+
if (!fileIno) {
|
|
712
|
+
throw ErrnoError.With('ENOENT', path, 'removeEntry');
|
|
713
|
+
}
|
|
714
|
+
// Get file inode.
|
|
715
|
+
const fileNode = this.getINodeSync(tx, fileIno, path);
|
|
716
|
+
if (!fileNode.toStats().hasAccess(W_OK, cred)) {
|
|
717
|
+
throw ErrnoError.With('EACCES', path, 'removeEntry');
|
|
718
|
+
}
|
|
719
|
+
// Remove from directory listing of parent.
|
|
720
|
+
delete listing[fileName];
|
|
721
|
+
if (!isDir && fileNode.toStats().isDirectory()) {
|
|
722
|
+
throw ErrnoError.With('EISDIR', path, 'removeEntry');
|
|
723
|
+
}
|
|
724
|
+
if (isDir && !fileNode.toStats().isDirectory()) {
|
|
725
|
+
throw ErrnoError.With('ENOTDIR', path, 'removeEntry');
|
|
726
|
+
}
|
|
727
|
+
try {
|
|
728
|
+
// Update directory listing.
|
|
729
|
+
tx.setSync(parentNode.ino, encodeDirListing(listing));
|
|
730
|
+
if (--fileNode.nlink < 1) {
|
|
731
|
+
// remove file
|
|
732
|
+
tx.removeSync(fileNode.ino);
|
|
733
|
+
tx.removeSync(fileIno);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
catch (e) {
|
|
737
|
+
tx.abortSync();
|
|
738
|
+
throw e;
|
|
739
|
+
}
|
|
740
|
+
// Success.
|
|
741
|
+
tx.commitSync();
|
|
742
|
+
}
|
|
743
|
+
}
|