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