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