@zenfs/core 0.2.1 → 0.2.3
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/AsyncStore.d.ts +4 -3
- package/dist/backends/AsyncStore.js +6 -0
- package/dist/backends/InMemory.d.ts +3 -9
- package/dist/backends/InMemory.js +4 -17
- package/dist/backends/Overlay.d.ts +110 -0
- package/dist/backends/Overlay.js +544 -0
- package/dist/backends/SyncStore.js +1 -5
- package/dist/backends/backend.d.ts +1 -2
- package/dist/backends/backend.js +4 -1
- package/dist/backends/index.d.ts +1 -1
- package/dist/backends/index.js +1 -1
- package/dist/browser.min.js +5 -5
- package/dist/browser.min.js.map +4 -4
- package/dist/emulation/index.d.ts +1 -1
- package/dist/emulation/index.js +1 -1
- package/dist/emulation/shared.d.ts +26 -8
- package/dist/emulation/shared.js +26 -12
- package/dist/filesystem.d.ts +18 -0
- package/dist/filesystem.js +46 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +2 -3
- package/dist/utils.d.ts +1 -9
- package/dist/utils.js +5 -5
- package/package.json +1 -1
- package/readme.md +40 -108
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import { FileSystem } from '../filesystem.js';
|
|
2
|
+
import { ApiError, ErrorCode } from '../ApiError.js';
|
|
3
|
+
import { FileFlag, PreloadFile } from '../file.js';
|
|
4
|
+
import { Stats } from '../stats.js';
|
|
5
|
+
import LockedFS from './Locked.js';
|
|
6
|
+
import { dirname } from '../emulation/path.js';
|
|
7
|
+
import { Cred } from '../cred.js';
|
|
8
|
+
import { decode, encode } from '../utils.js';
|
|
9
|
+
/**
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
const deletionLogPath = '/.deleted';
|
|
13
|
+
/**
|
|
14
|
+
* Overlays a RO file to make it writable.
|
|
15
|
+
*/
|
|
16
|
+
class OverlayFile extends PreloadFile {
|
|
17
|
+
constructor(fs, path, flag, stats, data) {
|
|
18
|
+
super(fs, path, flag, stats, data);
|
|
19
|
+
}
|
|
20
|
+
async sync() {
|
|
21
|
+
if (!this.isDirty()) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
await this.fs.sync(this.path, this.buffer, this.stats);
|
|
25
|
+
this.resetDirty();
|
|
26
|
+
}
|
|
27
|
+
syncSync() {
|
|
28
|
+
if (this.isDirty()) {
|
|
29
|
+
this.fs.syncSync(this.path, this.buffer, this.stats);
|
|
30
|
+
this.resetDirty();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async close() {
|
|
34
|
+
await this.sync();
|
|
35
|
+
}
|
|
36
|
+
closeSync() {
|
|
37
|
+
this.syncSync();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* OverlayFS makes a read-only filesystem writable by storing writes on a second, writable file system.
|
|
42
|
+
* Deletes are persisted via metadata stored on the writable file system.
|
|
43
|
+
*
|
|
44
|
+
* This class contains no locking whatsoever. It is wrapped in a LockedFS to prevent races.
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export class UnlockedOverlayFS extends FileSystem {
|
|
49
|
+
async ready() {
|
|
50
|
+
await this._readable.ready();
|
|
51
|
+
await this._writable.ready();
|
|
52
|
+
await this._ready;
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
constructor({ writable, readable }) {
|
|
56
|
+
super();
|
|
57
|
+
this._isInitialized = false;
|
|
58
|
+
this._deletedFiles = new Set();
|
|
59
|
+
this._deleteLog = '';
|
|
60
|
+
// If 'true', we have scheduled a delete log update.
|
|
61
|
+
this._deleteLogUpdatePending = false;
|
|
62
|
+
// If 'true', a delete log update is needed after the scheduled delete log
|
|
63
|
+
// update finishes.
|
|
64
|
+
this._deleteLogUpdateNeeded = false;
|
|
65
|
+
this._writable = writable;
|
|
66
|
+
this._readable = readable;
|
|
67
|
+
if (this._writable.metadata.readonly) {
|
|
68
|
+
throw new ApiError(ErrorCode.EINVAL, 'Writable file system must be writable.');
|
|
69
|
+
}
|
|
70
|
+
this._ready = this._initialize();
|
|
71
|
+
}
|
|
72
|
+
get metadata() {
|
|
73
|
+
return {
|
|
74
|
+
...super.metadata,
|
|
75
|
+
name: OverlayFS.name,
|
|
76
|
+
synchronous: this._readable.metadata.synchronous && this._writable.metadata.synchronous,
|
|
77
|
+
supportsProperties: this._readable.metadata.supportsProperties && this._writable.metadata.supportsProperties,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
getOverlayedFileSystems() {
|
|
81
|
+
return {
|
|
82
|
+
readable: this._readable,
|
|
83
|
+
writable: this._writable,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async sync(path, data, stats) {
|
|
87
|
+
const cred = stats.getCred(0, 0);
|
|
88
|
+
await this.createParentDirectories(path, cred);
|
|
89
|
+
await this._writable.sync(path, data, stats);
|
|
90
|
+
}
|
|
91
|
+
syncSync(path, data, stats) {
|
|
92
|
+
const cred = stats.getCred(0, 0);
|
|
93
|
+
this.createParentDirectoriesSync(path, cred);
|
|
94
|
+
this._writable.syncSync(path, data, stats);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Called once to load up metadata stored on the writable file system.
|
|
98
|
+
* @internal
|
|
99
|
+
*/
|
|
100
|
+
async _initialize() {
|
|
101
|
+
if (this._isInitialized) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Read deletion log, process into metadata.
|
|
105
|
+
try {
|
|
106
|
+
const file = await this._writable.openFile(deletionLogPath, FileFlag.FromString('r'), Cred.Root);
|
|
107
|
+
const { size } = await file.stat();
|
|
108
|
+
const { buffer } = await file.read(new Uint8Array(size));
|
|
109
|
+
this._deleteLog = decode(buffer);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
if (err.errno !== ErrorCode.ENOENT) {
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
this._isInitialized = true;
|
|
117
|
+
this._reparseDeletionLog();
|
|
118
|
+
}
|
|
119
|
+
getDeletionLog() {
|
|
120
|
+
return this._deleteLog;
|
|
121
|
+
}
|
|
122
|
+
restoreDeletionLog(log, cred) {
|
|
123
|
+
this._deleteLog = log;
|
|
124
|
+
this._reparseDeletionLog();
|
|
125
|
+
this.updateLog('', cred);
|
|
126
|
+
}
|
|
127
|
+
async rename(oldPath, newPath, cred) {
|
|
128
|
+
this.checkInitialized();
|
|
129
|
+
this.checkPath(oldPath);
|
|
130
|
+
this.checkPath(newPath);
|
|
131
|
+
try {
|
|
132
|
+
await this._writable.rename(oldPath, newPath, cred);
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
if (this._deletedFiles.has(oldPath)) {
|
|
136
|
+
throw ApiError.ENOENT(oldPath);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
renameSync(oldPath, newPath, cred) {
|
|
141
|
+
this.checkInitialized();
|
|
142
|
+
this.checkPath(oldPath);
|
|
143
|
+
this.checkPath(newPath);
|
|
144
|
+
try {
|
|
145
|
+
this._writable.renameSync(oldPath, newPath, cred);
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
if (this._deletedFiles.has(oldPath)) {
|
|
149
|
+
throw ApiError.ENOENT(oldPath);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async stat(p, cred) {
|
|
154
|
+
this.checkInitialized();
|
|
155
|
+
try {
|
|
156
|
+
return this._writable.stat(p, cred);
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
if (this._deletedFiles.has(p)) {
|
|
160
|
+
throw ApiError.ENOENT(p);
|
|
161
|
+
}
|
|
162
|
+
const oldStat = Stats.clone(await this._readable.stat(p, cred));
|
|
163
|
+
// Make the oldStat's mode writable. Preserve the topmost part of the
|
|
164
|
+
// mode, which specifies if it is a file or a directory.
|
|
165
|
+
oldStat.mode |= 0o222;
|
|
166
|
+
return oldStat;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
statSync(p, cred) {
|
|
170
|
+
this.checkInitialized();
|
|
171
|
+
try {
|
|
172
|
+
return this._writable.statSync(p, cred);
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
if (this._deletedFiles.has(p)) {
|
|
176
|
+
throw ApiError.ENOENT(p);
|
|
177
|
+
}
|
|
178
|
+
const oldStat = Stats.clone(this._readable.statSync(p, cred));
|
|
179
|
+
// Make the oldStat's mode writable. Preserve the topmost part of the
|
|
180
|
+
// mode, which specifies if it is a file or a directory.
|
|
181
|
+
oldStat.mode |= 0o222;
|
|
182
|
+
return oldStat;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async openFile(path, flag, cred) {
|
|
186
|
+
if (await this._writable.exists(path, cred)) {
|
|
187
|
+
return this._writable.openFile(path, flag, cred);
|
|
188
|
+
}
|
|
189
|
+
// Create an OverlayFile.
|
|
190
|
+
const file = await this._readable.openFile(path, FileFlag.FromString('r'), cred);
|
|
191
|
+
const stats = Stats.clone(await file.stat());
|
|
192
|
+
const { buffer } = await file.read(new Uint8Array(stats.size));
|
|
193
|
+
return new OverlayFile(this, path, flag, stats, buffer);
|
|
194
|
+
}
|
|
195
|
+
openFileSync(path, flag, cred) {
|
|
196
|
+
if (this._writable.existsSync(path, cred)) {
|
|
197
|
+
return this._writable.openFileSync(path, flag, cred);
|
|
198
|
+
}
|
|
199
|
+
// Create an OverlayFile.
|
|
200
|
+
const file = this._readable.openFileSync(path, FileFlag.FromString('r'), cred);
|
|
201
|
+
const stats = Stats.clone(file.statSync());
|
|
202
|
+
const data = new Uint8Array(stats.size);
|
|
203
|
+
file.readSync(data);
|
|
204
|
+
return new OverlayFile(this, path, flag, stats, data);
|
|
205
|
+
}
|
|
206
|
+
async createFile(path, flag, mode, cred) {
|
|
207
|
+
this.checkInitialized();
|
|
208
|
+
await this._writable.createFile(path, flag, mode, cred);
|
|
209
|
+
return this.openFile(path, flag, cred);
|
|
210
|
+
}
|
|
211
|
+
createFileSync(path, flag, mode, cred) {
|
|
212
|
+
this.checkInitialized();
|
|
213
|
+
this._writable.createFileSync(path, flag, mode, cred);
|
|
214
|
+
return this.openFileSync(path, flag, cred);
|
|
215
|
+
}
|
|
216
|
+
async link(srcpath, dstpath, cred) {
|
|
217
|
+
this.checkInitialized();
|
|
218
|
+
await this._writable.link(srcpath, dstpath, cred);
|
|
219
|
+
}
|
|
220
|
+
linkSync(srcpath, dstpath, cred) {
|
|
221
|
+
this.checkInitialized();
|
|
222
|
+
this._writable.linkSync(srcpath, dstpath, cred);
|
|
223
|
+
}
|
|
224
|
+
async unlink(p, cred) {
|
|
225
|
+
this.checkInitialized();
|
|
226
|
+
this.checkPath(p);
|
|
227
|
+
if (!(await this.exists(p, cred))) {
|
|
228
|
+
throw ApiError.ENOENT(p);
|
|
229
|
+
}
|
|
230
|
+
if (await this._writable.exists(p, cred)) {
|
|
231
|
+
await this._writable.unlink(p, cred);
|
|
232
|
+
}
|
|
233
|
+
// if it still exists add to the delete log
|
|
234
|
+
if (await this.exists(p, cred)) {
|
|
235
|
+
this.deletePath(p, cred);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
unlinkSync(p, cred) {
|
|
239
|
+
this.checkInitialized();
|
|
240
|
+
this.checkPath(p);
|
|
241
|
+
if (!this.existsSync(p, cred)) {
|
|
242
|
+
throw ApiError.ENOENT(p);
|
|
243
|
+
}
|
|
244
|
+
if (this._writable.existsSync(p, cred)) {
|
|
245
|
+
this._writable.unlinkSync(p, cred);
|
|
246
|
+
}
|
|
247
|
+
// if it still exists add to the delete log
|
|
248
|
+
if (this.existsSync(p, cred)) {
|
|
249
|
+
this.deletePath(p, cred);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async rmdir(p, cred) {
|
|
253
|
+
this.checkInitialized();
|
|
254
|
+
if (!(await this.exists(p, cred))) {
|
|
255
|
+
throw ApiError.ENOENT(p);
|
|
256
|
+
}
|
|
257
|
+
if (await this._writable.exists(p, cred)) {
|
|
258
|
+
await this._writable.rmdir(p, cred);
|
|
259
|
+
}
|
|
260
|
+
if (await this.exists(p, cred)) {
|
|
261
|
+
// Check if directory is empty.
|
|
262
|
+
if ((await this.readdir(p, cred)).length > 0) {
|
|
263
|
+
throw ApiError.ENOTEMPTY(p);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
this.deletePath(p, cred);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
rmdirSync(p, cred) {
|
|
271
|
+
this.checkInitialized();
|
|
272
|
+
if (!this.existsSync(p, cred)) {
|
|
273
|
+
throw ApiError.ENOENT(p);
|
|
274
|
+
}
|
|
275
|
+
if (this._writable.existsSync(p, cred)) {
|
|
276
|
+
this._writable.rmdirSync(p, cred);
|
|
277
|
+
}
|
|
278
|
+
if (this.existsSync(p, cred)) {
|
|
279
|
+
// Check if directory is empty.
|
|
280
|
+
if (this.readdirSync(p, cred).length > 0) {
|
|
281
|
+
throw ApiError.ENOTEMPTY(p);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
this.deletePath(p, cred);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async mkdir(p, mode, cred) {
|
|
289
|
+
this.checkInitialized();
|
|
290
|
+
if (await this.exists(p, cred)) {
|
|
291
|
+
throw ApiError.EEXIST(p);
|
|
292
|
+
}
|
|
293
|
+
// The below will throw should any of the parent directories fail to exist on _writable.
|
|
294
|
+
await this.createParentDirectories(p, cred);
|
|
295
|
+
await this._writable.mkdir(p, mode, cred);
|
|
296
|
+
}
|
|
297
|
+
mkdirSync(p, mode, cred) {
|
|
298
|
+
this.checkInitialized();
|
|
299
|
+
if (this.existsSync(p, cred)) {
|
|
300
|
+
throw ApiError.EEXIST(p);
|
|
301
|
+
}
|
|
302
|
+
// The below will throw should any of the parent directories fail to exist on _writable.
|
|
303
|
+
this.createParentDirectoriesSync(p, cred);
|
|
304
|
+
this._writable.mkdirSync(p, mode, cred);
|
|
305
|
+
}
|
|
306
|
+
async readdir(p, cred) {
|
|
307
|
+
this.checkInitialized();
|
|
308
|
+
const dirStats = await this.stat(p, cred);
|
|
309
|
+
if (!dirStats.isDirectory()) {
|
|
310
|
+
throw ApiError.ENOTDIR(p);
|
|
311
|
+
}
|
|
312
|
+
// Readdir in both, check delete log on RO file system's listing, merge, return.
|
|
313
|
+
const contents = [];
|
|
314
|
+
try {
|
|
315
|
+
contents.push(...(await this._writable.readdir(p, cred)));
|
|
316
|
+
}
|
|
317
|
+
catch (e) {
|
|
318
|
+
// NOP.
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
contents.push(...(await this._readable.readdir(p, cred)).filter((fPath) => !this._deletedFiles.has(`${p}/${fPath}`)));
|
|
322
|
+
}
|
|
323
|
+
catch (e) {
|
|
324
|
+
// NOP.
|
|
325
|
+
}
|
|
326
|
+
const seenMap = {};
|
|
327
|
+
return contents.filter((fileP) => {
|
|
328
|
+
const result = !seenMap[fileP];
|
|
329
|
+
seenMap[fileP] = true;
|
|
330
|
+
return result;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
readdirSync(p, cred) {
|
|
334
|
+
this.checkInitialized();
|
|
335
|
+
const dirStats = this.statSync(p, cred);
|
|
336
|
+
if (!dirStats.isDirectory()) {
|
|
337
|
+
throw ApiError.ENOTDIR(p);
|
|
338
|
+
}
|
|
339
|
+
// Readdir in both, check delete log on RO file system's listing, merge, return.
|
|
340
|
+
let contents = [];
|
|
341
|
+
try {
|
|
342
|
+
contents = contents.concat(this._writable.readdirSync(p, cred));
|
|
343
|
+
}
|
|
344
|
+
catch (e) {
|
|
345
|
+
// NOP.
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
contents = contents.concat(this._readable.readdirSync(p, cred).filter((fPath) => !this._deletedFiles.has(`${p}/${fPath}`)));
|
|
349
|
+
}
|
|
350
|
+
catch (e) {
|
|
351
|
+
// NOP.
|
|
352
|
+
}
|
|
353
|
+
const seenMap = {};
|
|
354
|
+
return contents.filter((fileP) => {
|
|
355
|
+
const result = !seenMap[fileP];
|
|
356
|
+
seenMap[fileP] = true;
|
|
357
|
+
return result;
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
deletePath(p, cred) {
|
|
361
|
+
this._deletedFiles.add(p);
|
|
362
|
+
this.updateLog(`d${p}\n`, cred);
|
|
363
|
+
}
|
|
364
|
+
async updateLog(addition, cred) {
|
|
365
|
+
this._deleteLog += addition;
|
|
366
|
+
if (this._deleteLogUpdatePending) {
|
|
367
|
+
this._deleteLogUpdateNeeded = true;
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
this._deleteLogUpdatePending = true;
|
|
371
|
+
const log = await this._writable.openFile(deletionLogPath, FileFlag.FromString('w'), cred);
|
|
372
|
+
try {
|
|
373
|
+
await log.write(encode(this._deleteLog));
|
|
374
|
+
if (this._deleteLogUpdateNeeded) {
|
|
375
|
+
this._deleteLogUpdateNeeded = false;
|
|
376
|
+
this.updateLog('', cred);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
catch (e) {
|
|
380
|
+
this._deleteLogError = e;
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
this._deleteLogUpdatePending = false;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
_reparseDeletionLog() {
|
|
387
|
+
this._deletedFiles.clear();
|
|
388
|
+
for (const entry of this._deleteLog.split('\n')) {
|
|
389
|
+
if (!entry.startsWith('d')) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
// If the log entry begins w/ 'd', it's a deletion.
|
|
393
|
+
this._deletedFiles.add(entry.slice(1));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
checkInitialized() {
|
|
397
|
+
if (!this._isInitialized) {
|
|
398
|
+
throw new ApiError(ErrorCode.EPERM, 'OverlayFS is not initialized. Please initialize OverlayFS using its initialize() method before using it.');
|
|
399
|
+
}
|
|
400
|
+
if (!this._deleteLogError) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const error = this._deleteLogError;
|
|
404
|
+
this._deleteLogError = null;
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
checkPath(path) {
|
|
408
|
+
if (path == deletionLogPath) {
|
|
409
|
+
throw ApiError.EPERM(path);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* With the given path, create the needed parent directories on the writable storage
|
|
414
|
+
* should they not exist. Use modes from the read-only storage.
|
|
415
|
+
*/
|
|
416
|
+
createParentDirectoriesSync(p, cred) {
|
|
417
|
+
let parent = dirname(p), toCreate = [];
|
|
418
|
+
while (!this._writable.existsSync(parent, cred)) {
|
|
419
|
+
toCreate.push(parent);
|
|
420
|
+
parent = dirname(parent);
|
|
421
|
+
}
|
|
422
|
+
toCreate = toCreate.reverse();
|
|
423
|
+
for (const p of toCreate) {
|
|
424
|
+
this._writable.mkdirSync(p, this.statSync(p, cred).mode, cred);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async createParentDirectories(p, cred) {
|
|
428
|
+
let parent = dirname(p), toCreate = [];
|
|
429
|
+
while (!(await this._writable.exists(parent, cred))) {
|
|
430
|
+
toCreate.push(parent);
|
|
431
|
+
parent = dirname(parent);
|
|
432
|
+
}
|
|
433
|
+
toCreate = toCreate.reverse();
|
|
434
|
+
for (const p of toCreate) {
|
|
435
|
+
const stats = await this.stat(p, cred);
|
|
436
|
+
await this._writable.mkdir(p, stats.mode, cred);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Helper function:
|
|
441
|
+
* - Ensures p is on writable before proceeding. Throws an error if it doesn't exist.
|
|
442
|
+
* - Calls f to perform operation on writable.
|
|
443
|
+
*/
|
|
444
|
+
operateOnWritable(p, cred) {
|
|
445
|
+
if (!this.existsSync(p, cred)) {
|
|
446
|
+
throw ApiError.ENOENT(p);
|
|
447
|
+
}
|
|
448
|
+
if (!this._writable.existsSync(p, cred)) {
|
|
449
|
+
// File is on readable storage. Copy to writable storage before
|
|
450
|
+
// changing its mode.
|
|
451
|
+
this.copyToWritableSync(p, cred);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async operateOnWritableAsync(p, cred) {
|
|
455
|
+
if (!(await this.exists(p, cred))) {
|
|
456
|
+
throw ApiError.ENOENT(p);
|
|
457
|
+
}
|
|
458
|
+
if (!(await this._writable.exists(p, cred))) {
|
|
459
|
+
return this.copyToWritable(p, cred);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Copy from readable to writable storage.
|
|
464
|
+
* PRECONDITION: File does not exist on writable storage.
|
|
465
|
+
*/
|
|
466
|
+
copyToWritableSync(p, cred) {
|
|
467
|
+
const stats = this.statSync(p, cred);
|
|
468
|
+
if (stats.isDirectory()) {
|
|
469
|
+
this._writable.mkdirSync(p, stats.mode, cred);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const data = new Uint8Array(stats.size);
|
|
473
|
+
const readable = this._readable.openFileSync(p, FileFlag.FromString('r'), cred);
|
|
474
|
+
readable.readSync(data);
|
|
475
|
+
readable.closeSync();
|
|
476
|
+
const writable = this._writable.openFileSync(p, FileFlag.FromString('w'), cred);
|
|
477
|
+
writable.writeSync(data);
|
|
478
|
+
writable.closeSync();
|
|
479
|
+
}
|
|
480
|
+
async copyToWritable(p, cred) {
|
|
481
|
+
const stats = await this.stat(p, cred);
|
|
482
|
+
if (stats.isDirectory()) {
|
|
483
|
+
await this._writable.mkdir(p, stats.mode, cred);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const data = new Uint8Array(stats.size);
|
|
487
|
+
const readable = await this._readable.openFile(p, FileFlag.FromString('r'), cred);
|
|
488
|
+
await readable.read(data);
|
|
489
|
+
await readable.close();
|
|
490
|
+
const writable = await this._writable.openFile(p, FileFlag.FromString('w'), cred);
|
|
491
|
+
await writable.write(data);
|
|
492
|
+
await writable.close();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* OverlayFS makes a read-only filesystem writable by storing writes on a second,
|
|
497
|
+
* writable file system. Deletes are persisted via metadata stored on the writable
|
|
498
|
+
* file system.
|
|
499
|
+
*/
|
|
500
|
+
export class OverlayFS extends LockedFS {
|
|
501
|
+
async ready() {
|
|
502
|
+
await super.ready();
|
|
503
|
+
return this;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* @param options The options to initialize the OverlayFS with
|
|
507
|
+
*/
|
|
508
|
+
constructor(options) {
|
|
509
|
+
super(new UnlockedOverlayFS(options));
|
|
510
|
+
}
|
|
511
|
+
getOverlayedFileSystems() {
|
|
512
|
+
return super.fs.getOverlayedFileSystems();
|
|
513
|
+
}
|
|
514
|
+
getDeletionLog() {
|
|
515
|
+
return super.fs.getDeletionLog();
|
|
516
|
+
}
|
|
517
|
+
resDeletionLog() {
|
|
518
|
+
return super.fs.getDeletionLog();
|
|
519
|
+
}
|
|
520
|
+
unwrap() {
|
|
521
|
+
return super.fs;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
export const Overlay = {
|
|
525
|
+
name: 'Overlay',
|
|
526
|
+
options: {
|
|
527
|
+
writable: {
|
|
528
|
+
type: 'object',
|
|
529
|
+
required: true,
|
|
530
|
+
description: 'The file system to write modified files to.',
|
|
531
|
+
},
|
|
532
|
+
readable: {
|
|
533
|
+
type: 'object',
|
|
534
|
+
required: true,
|
|
535
|
+
description: 'The file system that initially populates this file system.',
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
isAvailable() {
|
|
539
|
+
return true;
|
|
540
|
+
},
|
|
541
|
+
create(options) {
|
|
542
|
+
return new OverlayFS(options);
|
|
543
|
+
},
|
|
544
|
+
};
|
|
@@ -114,12 +114,8 @@ export class SyncStoreFileSystem extends SyncFileSystem {
|
|
|
114
114
|
}
|
|
115
115
|
get metadata() {
|
|
116
116
|
return {
|
|
117
|
+
...super.metadata,
|
|
117
118
|
name: this.store.name,
|
|
118
|
-
readonly: false,
|
|
119
|
-
supportsProperties: true,
|
|
120
|
-
synchronous: true,
|
|
121
|
-
freeSpace: 0,
|
|
122
|
-
totalSpace: 0,
|
|
123
119
|
};
|
|
124
120
|
}
|
|
125
121
|
/**
|
|
@@ -9,8 +9,7 @@ export interface OptionConfig<T> {
|
|
|
9
9
|
*/
|
|
10
10
|
type: OptionType | OptionType[];
|
|
11
11
|
/**
|
|
12
|
-
* Whether or not the option is required (optional can be set to null or undefined).
|
|
13
|
-
* Defaults to false.
|
|
12
|
+
* Whether or not the option is required (optional can be set to null or undefined). Defaults to false.
|
|
14
13
|
*/
|
|
15
14
|
required?: boolean;
|
|
16
15
|
/**
|
package/dist/backends/backend.js
CHANGED
|
@@ -80,5 +80,8 @@ export async function resolveBackendConfig(options) {
|
|
|
80
80
|
if (!backend) {
|
|
81
81
|
throw new ApiError(ErrorCode.EPERM, `Backend "${backend}" is not available`);
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
checkOptions(backend, options);
|
|
84
|
+
const fs = backend.create(options);
|
|
85
|
+
await fs.ready();
|
|
86
|
+
return fs;
|
|
84
87
|
}
|
package/dist/backends/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AsyncMirror } from './AsyncMirror.js';
|
|
2
2
|
import { InMemory } from './InMemory.js';
|
|
3
|
-
import { Overlay } from './
|
|
3
|
+
import { Overlay } from './Overlay.js';
|
|
4
4
|
import { Backend } from './backend.js';
|
|
5
5
|
export declare const backends: {
|
|
6
6
|
[backend: string]: Backend;
|
package/dist/backends/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AsyncMirror } from './AsyncMirror.js';
|
|
2
2
|
import { InMemory } from './InMemory.js';
|
|
3
|
-
import { Overlay } from './
|
|
3
|
+
import { Overlay } from './Overlay.js';
|
|
4
4
|
export const backends = {};
|
|
5
5
|
export default backends;
|
|
6
6
|
export { AsyncMirror, InMemory, Overlay };
|