@zenfs/core 0.0.1
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/README.md +293 -0
- package/dist/ApiError.d.ts +86 -0
- package/dist/ApiError.js +135 -0
- package/dist/backends/AsyncMirror.d.ts +102 -0
- package/dist/backends/AsyncMirror.js +252 -0
- package/dist/backends/AsyncStore.d.ts +166 -0
- package/dist/backends/AsyncStore.js +620 -0
- package/dist/backends/FolderAdapter.d.ts +52 -0
- package/dist/backends/FolderAdapter.js +184 -0
- package/dist/backends/InMemory.d.ts +25 -0
- package/dist/backends/InMemory.js +46 -0
- package/dist/backends/Locked.d.ts +64 -0
- package/dist/backends/Locked.js +302 -0
- package/dist/backends/OverlayFS.d.ts +120 -0
- package/dist/backends/OverlayFS.js +749 -0
- package/dist/backends/SyncStore.d.ts +223 -0
- package/dist/backends/SyncStore.js +479 -0
- package/dist/backends/backend.d.ts +73 -0
- package/dist/backends/backend.js +14 -0
- package/dist/backends/index.d.ts +11 -0
- package/dist/backends/index.js +15 -0
- package/dist/browser.min.js +12 -0
- package/dist/browser.min.js.map +7 -0
- package/dist/cred.d.ts +14 -0
- package/dist/cred.js +15 -0
- package/dist/emulation/callbacks.d.ts +382 -0
- package/dist/emulation/callbacks.js +422 -0
- package/dist/emulation/constants.d.ts +101 -0
- package/dist/emulation/constants.js +110 -0
- package/dist/emulation/fs.d.ts +7 -0
- package/dist/emulation/fs.js +5 -0
- package/dist/emulation/index.d.ts +5 -0
- package/dist/emulation/index.js +7 -0
- package/dist/emulation/promises.d.ts +309 -0
- package/dist/emulation/promises.js +521 -0
- package/dist/emulation/shared.d.ts +62 -0
- package/dist/emulation/shared.js +192 -0
- package/dist/emulation/sync.d.ts +278 -0
- package/dist/emulation/sync.js +392 -0
- package/dist/file.d.ts +449 -0
- package/dist/file.js +576 -0
- package/dist/filesystem.d.ts +367 -0
- package/dist/filesystem.js +542 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +113 -0
- package/dist/inode.d.ts +51 -0
- package/dist/inode.js +112 -0
- package/dist/mutex.d.ts +12 -0
- package/dist/mutex.js +48 -0
- package/dist/stats.d.ts +98 -0
- package/dist/stats.js +226 -0
- package/dist/utils.d.ts +52 -0
- package/dist/utils.js +261 -0
- package/license.md +122 -0
- package/package.json +61 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { ApiError, ErrorCode } from '../ApiError';
|
|
12
|
+
import { W_OK, R_OK } from '../emulation/constants';
|
|
13
|
+
import { PreloadFile, FileFlag } from '../file';
|
|
14
|
+
import { BaseFileSystem } from '../filesystem';
|
|
15
|
+
import Inode from '../inode';
|
|
16
|
+
import { FileType } from '../stats';
|
|
17
|
+
import { ROOT_NODE_ID, randomUUID, getEmptyDirNode } from '../utils';
|
|
18
|
+
class LRUNode {
|
|
19
|
+
constructor(key, value) {
|
|
20
|
+
this.key = key;
|
|
21
|
+
this.value = value;
|
|
22
|
+
this.prev = null;
|
|
23
|
+
this.next = null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Adapted from https://chrisrng.svbtle.com/lru-cache-in-javascript
|
|
27
|
+
class LRUCache {
|
|
28
|
+
constructor(limit) {
|
|
29
|
+
this.limit = limit;
|
|
30
|
+
this.size = 0;
|
|
31
|
+
this.map = {};
|
|
32
|
+
this.head = null;
|
|
33
|
+
this.tail = null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Change or add a new value in the cache
|
|
37
|
+
* We overwrite the entry if it already exists
|
|
38
|
+
*/
|
|
39
|
+
set(key, value) {
|
|
40
|
+
const node = new LRUNode(key, value);
|
|
41
|
+
if (this.map[key]) {
|
|
42
|
+
this.map[key].value = node.value;
|
|
43
|
+
this.remove(node.key);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
if (this.size >= this.limit) {
|
|
47
|
+
delete this.map[this.tail.key];
|
|
48
|
+
this.size--;
|
|
49
|
+
this.tail = this.tail.prev;
|
|
50
|
+
this.tail.next = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
this.setHead(node);
|
|
54
|
+
}
|
|
55
|
+
/* Retrieve a single entry from the cache */
|
|
56
|
+
get(key) {
|
|
57
|
+
if (this.map[key]) {
|
|
58
|
+
const value = this.map[key].value;
|
|
59
|
+
const node = new LRUNode(key, value);
|
|
60
|
+
this.remove(key);
|
|
61
|
+
this.setHead(node);
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/* Remove a single entry from the cache */
|
|
69
|
+
remove(key) {
|
|
70
|
+
const node = this.map[key];
|
|
71
|
+
if (!node) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (node.prev !== null) {
|
|
75
|
+
node.prev.next = node.next;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
this.head = node.next;
|
|
79
|
+
}
|
|
80
|
+
if (node.next !== null) {
|
|
81
|
+
node.next.prev = node.prev;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
this.tail = node.prev;
|
|
85
|
+
}
|
|
86
|
+
delete this.map[key];
|
|
87
|
+
this.size--;
|
|
88
|
+
}
|
|
89
|
+
/* Resets the entire cache - Argument limit is optional to be reset */
|
|
90
|
+
removeAll() {
|
|
91
|
+
this.size = 0;
|
|
92
|
+
this.map = {};
|
|
93
|
+
this.head = null;
|
|
94
|
+
this.tail = null;
|
|
95
|
+
}
|
|
96
|
+
setHead(node) {
|
|
97
|
+
node.next = this.head;
|
|
98
|
+
node.prev = null;
|
|
99
|
+
if (this.head !== null) {
|
|
100
|
+
this.head.prev = node;
|
|
101
|
+
}
|
|
102
|
+
this.head = node;
|
|
103
|
+
if (this.tail === null) {
|
|
104
|
+
this.tail = node;
|
|
105
|
+
}
|
|
106
|
+
this.size++;
|
|
107
|
+
this.map[node.key] = node;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export class AsyncKeyValueFile extends PreloadFile {
|
|
111
|
+
constructor(_fs, _path, _flag, _stat, contents) {
|
|
112
|
+
super(_fs, _path, _flag, _stat, contents);
|
|
113
|
+
}
|
|
114
|
+
sync() {
|
|
115
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
116
|
+
if (!this.isDirty()) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
yield this._fs._sync(this.getPath(), this.getBuffer(), this.getStats());
|
|
120
|
+
this.resetDirty();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
close() {
|
|
124
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
125
|
+
this.sync();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* An "Asynchronous key-value file system". Stores data to/retrieves data from
|
|
131
|
+
* an underlying asynchronous key-value store.
|
|
132
|
+
*/
|
|
133
|
+
export class AsyncKeyValueFileSystem extends BaseFileSystem {
|
|
134
|
+
static isAvailable() {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
constructor(cacheSize) {
|
|
138
|
+
super();
|
|
139
|
+
this._cache = null;
|
|
140
|
+
if (cacheSize > 0) {
|
|
141
|
+
this._cache = new LRUCache(cacheSize);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Initializes the file system. Typically called by subclasses' async
|
|
146
|
+
* constructors.
|
|
147
|
+
*/
|
|
148
|
+
init(store) {
|
|
149
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
150
|
+
this.store = store;
|
|
151
|
+
// INVARIANT: Ensure that the root exists.
|
|
152
|
+
yield this.makeRootDirectory();
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
getName() {
|
|
156
|
+
return this.store.name();
|
|
157
|
+
}
|
|
158
|
+
isReadOnly() {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
supportsSymlinks() {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
supportsProps() {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
supportsSynch() {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Delete all contents stored in the file system.
|
|
172
|
+
*/
|
|
173
|
+
empty() {
|
|
174
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
175
|
+
if (this._cache) {
|
|
176
|
+
this._cache.removeAll();
|
|
177
|
+
}
|
|
178
|
+
yield this.store.clear();
|
|
179
|
+
// INVARIANT: Root always exists.
|
|
180
|
+
yield this.makeRootDirectory();
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
access(p, mode, cred) {
|
|
184
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
185
|
+
const tx = this.store.beginTransaction('readonly');
|
|
186
|
+
const inode = yield this.findINode(tx, p);
|
|
187
|
+
if (!inode) {
|
|
188
|
+
throw ApiError.ENOENT(p);
|
|
189
|
+
}
|
|
190
|
+
if (!inode.toStats().hasAccess(mode, cred)) {
|
|
191
|
+
throw ApiError.EACCES(p);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* @todo Make rename compatible with the cache.
|
|
197
|
+
*/
|
|
198
|
+
rename(oldPath, newPath, cred) {
|
|
199
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
200
|
+
const c = this._cache;
|
|
201
|
+
if (this._cache) {
|
|
202
|
+
// Clear and disable cache during renaming process.
|
|
203
|
+
this._cache = null;
|
|
204
|
+
c.removeAll();
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
const tx = this.store.beginTransaction('readwrite'), oldParent = path.dirname(oldPath), oldName = path.basename(oldPath), newParent = path.dirname(newPath), newName = path.basename(newPath),
|
|
208
|
+
// Remove oldPath from parent's directory listing.
|
|
209
|
+
oldDirNode = yield this.findINode(tx, oldParent), oldDirList = yield this.getDirListing(tx, oldParent, oldDirNode);
|
|
210
|
+
if (!oldDirNode.toStats().hasAccess(W_OK, cred)) {
|
|
211
|
+
throw ApiError.EACCES(oldPath);
|
|
212
|
+
}
|
|
213
|
+
if (!oldDirList[oldName]) {
|
|
214
|
+
throw ApiError.ENOENT(oldPath);
|
|
215
|
+
}
|
|
216
|
+
const nodeId = oldDirList[oldName];
|
|
217
|
+
delete oldDirList[oldName];
|
|
218
|
+
// Invariant: Can't move a folder inside itself.
|
|
219
|
+
// This funny little hack ensures that the check passes only if oldPath
|
|
220
|
+
// is a subpath of newParent. We append '/' to avoid matching folders that
|
|
221
|
+
// are a substring of the bottom-most folder in the path.
|
|
222
|
+
if ((newParent + '/').indexOf(oldPath + '/') === 0) {
|
|
223
|
+
throw new ApiError(ErrorCode.EBUSY, oldParent);
|
|
224
|
+
}
|
|
225
|
+
// Add newPath to parent's directory listing.
|
|
226
|
+
let newDirNode, newDirList;
|
|
227
|
+
if (newParent === oldParent) {
|
|
228
|
+
// Prevent us from re-grabbing the same directory listing, which still
|
|
229
|
+
// contains oldName.
|
|
230
|
+
newDirNode = oldDirNode;
|
|
231
|
+
newDirList = oldDirList;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
newDirNode = yield this.findINode(tx, newParent);
|
|
235
|
+
newDirList = yield this.getDirListing(tx, newParent, newDirNode);
|
|
236
|
+
}
|
|
237
|
+
if (newDirList[newName]) {
|
|
238
|
+
// If it's a file, delete it.
|
|
239
|
+
const newNameNode = yield this.getINode(tx, newPath, newDirList[newName]);
|
|
240
|
+
if (newNameNode.isFile()) {
|
|
241
|
+
try {
|
|
242
|
+
yield tx.del(newNameNode.id);
|
|
243
|
+
yield tx.del(newDirList[newName]);
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
yield tx.abort();
|
|
247
|
+
throw e;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
// If it's a directory, throw a permissions error.
|
|
252
|
+
throw ApiError.EPERM(newPath);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
newDirList[newName] = nodeId;
|
|
256
|
+
// Commit the two changed directory listings.
|
|
257
|
+
try {
|
|
258
|
+
yield tx.put(oldDirNode.id, Buffer.from(JSON.stringify(oldDirList)), true);
|
|
259
|
+
yield tx.put(newDirNode.id, Buffer.from(JSON.stringify(newDirList)), true);
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
yield tx.abort();
|
|
263
|
+
throw e;
|
|
264
|
+
}
|
|
265
|
+
yield tx.commit();
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
if (c) {
|
|
269
|
+
this._cache = c;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
stat(p, cred) {
|
|
275
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
276
|
+
const tx = this.store.beginTransaction('readonly');
|
|
277
|
+
const inode = yield this.findINode(tx, p);
|
|
278
|
+
const stats = inode.toStats();
|
|
279
|
+
if (!stats.hasAccess(R_OK, cred)) {
|
|
280
|
+
throw ApiError.EACCES(p);
|
|
281
|
+
}
|
|
282
|
+
return stats;
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
createFile(p, flag, mode, cred) {
|
|
286
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
287
|
+
const tx = this.store.beginTransaction('readwrite'), data = Buffer.alloc(0), newFile = yield this.commitNewFile(tx, p, FileType.FILE, mode, cred, data);
|
|
288
|
+
// Open the file.
|
|
289
|
+
return new AsyncKeyValueFile(this, p, flag, newFile.toStats(), data);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
openFile(p, flag, cred) {
|
|
293
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
294
|
+
const tx = this.store.beginTransaction('readonly'), node = yield this.findINode(tx, p), data = yield tx.get(node.id);
|
|
295
|
+
if (!node.toStats().hasAccess(flag.getMode(), cred)) {
|
|
296
|
+
throw ApiError.EACCES(p);
|
|
297
|
+
}
|
|
298
|
+
if (data === undefined) {
|
|
299
|
+
throw ApiError.ENOENT(p);
|
|
300
|
+
}
|
|
301
|
+
return new AsyncKeyValueFile(this, p, flag, node.toStats(), data);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
unlink(p, cred) {
|
|
305
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
306
|
+
return this.removeEntry(p, false, cred);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
rmdir(p, cred) {
|
|
310
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
311
|
+
// Check first if directory is empty.
|
|
312
|
+
const list = yield this.readdir(p, cred);
|
|
313
|
+
if (list.length > 0) {
|
|
314
|
+
throw ApiError.ENOTEMPTY(p);
|
|
315
|
+
}
|
|
316
|
+
yield this.removeEntry(p, true, cred);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
mkdir(p, mode, cred) {
|
|
320
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
321
|
+
const tx = this.store.beginTransaction('readwrite'), data = Buffer.from('{}');
|
|
322
|
+
yield this.commitNewFile(tx, p, FileType.DIRECTORY, mode, cred, data);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
readdir(p, cred) {
|
|
326
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
327
|
+
const tx = this.store.beginTransaction('readonly');
|
|
328
|
+
const node = yield this.findINode(tx, p);
|
|
329
|
+
if (!node.toStats().hasAccess(R_OK, cred)) {
|
|
330
|
+
throw ApiError.EACCES(p);
|
|
331
|
+
}
|
|
332
|
+
return Object.keys(yield this.getDirListing(tx, p, node));
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
chmod(p, mode, cred) {
|
|
336
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
337
|
+
const fd = yield this.openFile(p, FileFlag.getFileFlag('r+'), cred);
|
|
338
|
+
yield fd.chmod(mode);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
chown(p, new_uid, new_gid, cred) {
|
|
342
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
343
|
+
const fd = yield this.openFile(p, FileFlag.getFileFlag('r+'), cred);
|
|
344
|
+
yield fd.chown(new_uid, new_gid);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
_sync(p, data, stats) {
|
|
348
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
349
|
+
// @todo Ensure mtime updates properly, and use that to determine if a data
|
|
350
|
+
// update is required.
|
|
351
|
+
const tx = this.store.beginTransaction('readwrite'),
|
|
352
|
+
// We use the _findInode helper because we actually need the INode id.
|
|
353
|
+
fileInodeId = yield this._findINode(tx, path.dirname(p), path.basename(p)), fileInode = yield this.getINode(tx, p, fileInodeId), inodeChanged = fileInode.update(stats);
|
|
354
|
+
try {
|
|
355
|
+
// Sync data.
|
|
356
|
+
yield tx.put(fileInode.id, data, true);
|
|
357
|
+
// Sync metadata.
|
|
358
|
+
if (inodeChanged) {
|
|
359
|
+
yield tx.put(fileInodeId, fileInode.toBuffer(), true);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch (e) {
|
|
363
|
+
yield tx.abort();
|
|
364
|
+
throw e;
|
|
365
|
+
}
|
|
366
|
+
yield tx.commit();
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Checks if the root directory exists. Creates it if it doesn't.
|
|
371
|
+
*/
|
|
372
|
+
makeRootDirectory() {
|
|
373
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
374
|
+
const tx = this.store.beginTransaction('readwrite');
|
|
375
|
+
if ((yield tx.get(ROOT_NODE_ID)) === undefined) {
|
|
376
|
+
// Create new inode.
|
|
377
|
+
const currTime = new Date().getTime(),
|
|
378
|
+
// Mode 0666, owned by root:root
|
|
379
|
+
dirInode = new Inode(randomUUID(), 4096, 511 | FileType.DIRECTORY, currTime, currTime, currTime, 0, 0);
|
|
380
|
+
// If the root doesn't exist, the first random ID shouldn't exist,
|
|
381
|
+
// either.
|
|
382
|
+
yield tx.put(dirInode.id, getEmptyDirNode(), false);
|
|
383
|
+
yield tx.put(ROOT_NODE_ID, dirInode.toBuffer(), false);
|
|
384
|
+
yield tx.commit();
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Helper function for findINode.
|
|
390
|
+
* @param parent The parent directory of the file we are attempting to find.
|
|
391
|
+
* @param filename The filename of the inode we are attempting to find, minus
|
|
392
|
+
* the parent.
|
|
393
|
+
*/
|
|
394
|
+
_findINode(tx, parent, filename, visited = new Set()) {
|
|
395
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
396
|
+
const currentPath = path.posix.join(parent, filename);
|
|
397
|
+
if (visited.has(currentPath)) {
|
|
398
|
+
throw new ApiError(ErrorCode.EIO, 'Infinite loop detected while finding inode', currentPath);
|
|
399
|
+
}
|
|
400
|
+
visited.add(currentPath);
|
|
401
|
+
if (this._cache) {
|
|
402
|
+
const id = this._cache.get(currentPath);
|
|
403
|
+
if (id) {
|
|
404
|
+
return id;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (parent === '/') {
|
|
408
|
+
if (filename === '') {
|
|
409
|
+
// BASE CASE #1: Return the root's ID.
|
|
410
|
+
if (this._cache) {
|
|
411
|
+
this._cache.set(currentPath, ROOT_NODE_ID);
|
|
412
|
+
}
|
|
413
|
+
return ROOT_NODE_ID;
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
// BASE CASE #2: Find the item in the root node.
|
|
417
|
+
const inode = yield this.getINode(tx, parent, ROOT_NODE_ID);
|
|
418
|
+
const dirList = yield this.getDirListing(tx, parent, inode);
|
|
419
|
+
if (dirList[filename]) {
|
|
420
|
+
const id = dirList[filename];
|
|
421
|
+
if (this._cache) {
|
|
422
|
+
this._cache.set(currentPath, id);
|
|
423
|
+
}
|
|
424
|
+
return id;
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
throw ApiError.ENOENT(path.resolve(parent, filename));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
// Get the parent directory's INode, and find the file in its directory
|
|
433
|
+
// listing.
|
|
434
|
+
const inode = yield this.findINode(tx, parent, visited);
|
|
435
|
+
const dirList = yield this.getDirListing(tx, parent, inode);
|
|
436
|
+
if (dirList[filename]) {
|
|
437
|
+
const id = dirList[filename];
|
|
438
|
+
if (this._cache) {
|
|
439
|
+
this._cache.set(currentPath, id);
|
|
440
|
+
}
|
|
441
|
+
return id;
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
throw ApiError.ENOENT(path.resolve(parent, filename));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Finds the Inode of the given path.
|
|
451
|
+
* @param p The path to look up.
|
|
452
|
+
* @todo memoize/cache
|
|
453
|
+
*/
|
|
454
|
+
findINode(tx, p, visited = new Set()) {
|
|
455
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
456
|
+
const id = yield this._findINode(tx, path.dirname(p), path.basename(p), visited);
|
|
457
|
+
return this.getINode(tx, p, id);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Given the ID of a node, retrieves the corresponding Inode.
|
|
462
|
+
* @param tx The transaction to use.
|
|
463
|
+
* @param p The corresponding path to the file (used for error messages).
|
|
464
|
+
* @param id The ID to look up.
|
|
465
|
+
*/
|
|
466
|
+
getINode(tx, p, id) {
|
|
467
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
468
|
+
const data = yield tx.get(id);
|
|
469
|
+
if (!data) {
|
|
470
|
+
throw ApiError.ENOENT(p);
|
|
471
|
+
}
|
|
472
|
+
return Inode.fromBuffer(data);
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Given the Inode of a directory, retrieves the corresponding directory
|
|
477
|
+
* listing.
|
|
478
|
+
*/
|
|
479
|
+
getDirListing(tx, p, inode) {
|
|
480
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
481
|
+
if (!inode.isDirectory()) {
|
|
482
|
+
throw ApiError.ENOTDIR(p);
|
|
483
|
+
}
|
|
484
|
+
const data = yield tx.get(inode.id);
|
|
485
|
+
try {
|
|
486
|
+
return JSON.parse(data.toString());
|
|
487
|
+
}
|
|
488
|
+
catch (e) {
|
|
489
|
+
// Occurs when data is undefined, or corresponds to something other
|
|
490
|
+
// than a directory listing. The latter should never occur unless
|
|
491
|
+
// the file system is corrupted.
|
|
492
|
+
throw ApiError.ENOENT(p);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Adds a new node under a random ID. Retries 5 times before giving up in
|
|
498
|
+
* the exceedingly unlikely chance that we try to reuse a random GUID.
|
|
499
|
+
*/
|
|
500
|
+
addNewNode(tx, data) {
|
|
501
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
502
|
+
let retries = 0;
|
|
503
|
+
const reroll = () => __awaiter(this, void 0, void 0, function* () {
|
|
504
|
+
if (++retries === 5) {
|
|
505
|
+
// Max retries hit. Return with an error.
|
|
506
|
+
throw new ApiError(ErrorCode.EIO, 'Unable to commit data to key-value store.');
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
// Try again.
|
|
510
|
+
const currId = randomUUID();
|
|
511
|
+
const committed = yield tx.put(currId, data, false);
|
|
512
|
+
if (!committed) {
|
|
513
|
+
return reroll();
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
return currId;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
return reroll();
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Commits a new file (well, a FILE or a DIRECTORY) to the file system with
|
|
525
|
+
* the given mode.
|
|
526
|
+
* Note: This will commit the transaction.
|
|
527
|
+
* @param p The path to the new file.
|
|
528
|
+
* @param type The type of the new file.
|
|
529
|
+
* @param mode The mode to create the new file with.
|
|
530
|
+
* @param cred The UID/GID to create the file with
|
|
531
|
+
* @param data The data to store at the file's data node.
|
|
532
|
+
*/
|
|
533
|
+
commitNewFile(tx, p, type, mode, cred, data) {
|
|
534
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
535
|
+
const parentDir = path.dirname(p), fname = path.basename(p), parentNode = yield this.findINode(tx, parentDir), dirListing = yield this.getDirListing(tx, parentDir, parentNode), currTime = new Date().getTime();
|
|
536
|
+
//Check that the creater has correct access
|
|
537
|
+
if (!parentNode.toStats().hasAccess(W_OK, cred)) {
|
|
538
|
+
throw ApiError.EACCES(p);
|
|
539
|
+
}
|
|
540
|
+
// Invariant: The root always exists.
|
|
541
|
+
// If we don't check this prior to taking steps below, we will create a
|
|
542
|
+
// file with name '' in root should p == '/'.
|
|
543
|
+
if (p === '/') {
|
|
544
|
+
throw ApiError.EEXIST(p);
|
|
545
|
+
}
|
|
546
|
+
// Check if file already exists.
|
|
547
|
+
if (dirListing[fname]) {
|
|
548
|
+
yield tx.abort();
|
|
549
|
+
throw ApiError.EEXIST(p);
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
// Commit data.
|
|
553
|
+
const dataId = yield this.addNewNode(tx, data);
|
|
554
|
+
const fileNode = new Inode(dataId, data.length, mode | type, currTime, currTime, currTime, cred.uid, cred.gid);
|
|
555
|
+
// Commit file node.
|
|
556
|
+
const fileNodeId = yield this.addNewNode(tx, fileNode.toBuffer());
|
|
557
|
+
// Update and commit parent directory listing.
|
|
558
|
+
dirListing[fname] = fileNodeId;
|
|
559
|
+
yield tx.put(parentNode.id, Buffer.from(JSON.stringify(dirListing)), true);
|
|
560
|
+
yield tx.commit();
|
|
561
|
+
return fileNode;
|
|
562
|
+
}
|
|
563
|
+
catch (e) {
|
|
564
|
+
tx.abort();
|
|
565
|
+
throw e;
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Remove all traces of the given path from the file system.
|
|
571
|
+
* @param p The path to remove from the file system.
|
|
572
|
+
* @param isDir Does the path belong to a directory, or a file?
|
|
573
|
+
* @todo Update mtime.
|
|
574
|
+
*/
|
|
575
|
+
/**
|
|
576
|
+
* Remove all traces of the given path from the file system.
|
|
577
|
+
* @param p The path to remove from the file system.
|
|
578
|
+
* @param isDir Does the path belong to a directory, or a file?
|
|
579
|
+
* @todo Update mtime.
|
|
580
|
+
*/
|
|
581
|
+
removeEntry(p, isDir, cred) {
|
|
582
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
583
|
+
if (this._cache) {
|
|
584
|
+
this._cache.remove(p);
|
|
585
|
+
}
|
|
586
|
+
const tx = this.store.beginTransaction('readwrite'), parent = path.dirname(p), parentNode = yield this.findINode(tx, parent), parentListing = yield this.getDirListing(tx, parent, parentNode), fileName = path.basename(p);
|
|
587
|
+
if (!parentListing[fileName]) {
|
|
588
|
+
throw ApiError.ENOENT(p);
|
|
589
|
+
}
|
|
590
|
+
const fileNodeId = parentListing[fileName];
|
|
591
|
+
// Get file inode.
|
|
592
|
+
const fileNode = yield this.getINode(tx, p, fileNodeId);
|
|
593
|
+
if (!fileNode.toStats().hasAccess(W_OK, cred)) {
|
|
594
|
+
throw ApiError.EACCES(p);
|
|
595
|
+
}
|
|
596
|
+
// Remove from directory listing of parent.
|
|
597
|
+
delete parentListing[fileName];
|
|
598
|
+
if (!isDir && fileNode.isDirectory()) {
|
|
599
|
+
throw ApiError.EISDIR(p);
|
|
600
|
+
}
|
|
601
|
+
else if (isDir && !fileNode.isDirectory()) {
|
|
602
|
+
throw ApiError.ENOTDIR(p);
|
|
603
|
+
}
|
|
604
|
+
try {
|
|
605
|
+
// Delete data.
|
|
606
|
+
yield tx.del(fileNode.id);
|
|
607
|
+
// Delete node.
|
|
608
|
+
yield tx.del(fileNodeId);
|
|
609
|
+
// Update directory listing.
|
|
610
|
+
yield tx.put(parentNode.id, Buffer.from(JSON.stringify(parentListing)), true);
|
|
611
|
+
}
|
|
612
|
+
catch (e) {
|
|
613
|
+
yield tx.abort();
|
|
614
|
+
throw e;
|
|
615
|
+
}
|
|
616
|
+
// Success.
|
|
617
|
+
yield tx.commit();
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { BaseFileSystem, type FileSystem } from '../filesystem';
|
|
2
|
+
import { type BackendOptions } from './backend';
|
|
3
|
+
export declare namespace FolderAdapter {
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for a FolderAdapter file system.
|
|
6
|
+
*/
|
|
7
|
+
interface Options {
|
|
8
|
+
folder: string;
|
|
9
|
+
wrapped: FileSystem;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The FolderAdapter file system wraps a file system, and scopes all interactions to a subfolder of that file system.
|
|
14
|
+
*
|
|
15
|
+
* Example: Given a file system `foo` with folder `bar` and file `bar/baz`...
|
|
16
|
+
*
|
|
17
|
+
* ```javascript
|
|
18
|
+
* ZenFS.configure({
|
|
19
|
+
* fs: "FolderAdapter",
|
|
20
|
+
* options: {
|
|
21
|
+
* folder: "bar",
|
|
22
|
+
* wrapped: foo
|
|
23
|
+
* }
|
|
24
|
+
* }, function(e) {
|
|
25
|
+
* var fs = ZenFS.BFSRequire('fs');
|
|
26
|
+
* fs.readdirSync('/'); // ['baz']
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare class FolderAdapter extends BaseFileSystem {
|
|
31
|
+
static readonly Name = "FolderAdapter";
|
|
32
|
+
static Create: any;
|
|
33
|
+
static readonly Options: BackendOptions;
|
|
34
|
+
static isAvailable(): boolean;
|
|
35
|
+
_wrapped: FileSystem;
|
|
36
|
+
_folder: string;
|
|
37
|
+
constructor({ folder, wrapped }: FolderAdapter.Options);
|
|
38
|
+
get metadata(): {
|
|
39
|
+
supportsLinks: boolean;
|
|
40
|
+
name: string;
|
|
41
|
+
readonly: boolean;
|
|
42
|
+
synchronous: boolean;
|
|
43
|
+
supportsProperties: boolean;
|
|
44
|
+
totalSpace: number;
|
|
45
|
+
freeSpace: number;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Initialize the file system. Ensures that the wrapped file system
|
|
49
|
+
* has the given folder.
|
|
50
|
+
*/
|
|
51
|
+
private _initialize;
|
|
52
|
+
}
|