@zenfs/core 1.10.1 → 1.10.2

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.
@@ -1,42 +1,87 @@
1
1
  import type { File } from '../internal/file.js';
2
2
  import type { CreationOptions, UsageInfo } from '../internal/filesystem.js';
3
- import type { Stats } from '../stats.js';
4
3
  import type { InodeLike } from '../internal/inode.js';
4
+ import type { Stats } from '../stats.js';
5
5
  import { FileSystem } from '../internal/filesystem.js';
6
6
  /**
7
- * Configuration options for OverlayFS instances.
7
+ * Configuration options for CoW.
8
8
  * @category Backends and Configuration
9
9
  */
10
- export interface OverlayOptions {
11
- /**
12
- * The file system to write modified files to.
13
- */
10
+ export interface CopyOnWriteOptions {
11
+ /** The file system that initially populates this file system. */
12
+ readable: FileSystem;
13
+ /** The file system to write modified files to. */
14
14
  writable: FileSystem;
15
+ /** Options for the journal */
16
+ journal?: JournalOptions;
17
+ }
18
+ /**
19
+ * @hidden @deprecated use `CopyOnWriteOptions`
20
+ */
21
+ export type OverlayOptions = CopyOnWriteOptions;
22
+ /**
23
+ * Configuration options for the journal used by CoW.
24
+ * @category Backends and Configuration
25
+ * @internal
26
+ */
27
+ export interface JournalOptions {
28
+ /** The contents of the journal */
29
+ contents?: string | JournalEntry[];
30
+ }
31
+ declare const journalOperations: readonly ["delete"];
32
+ /**
33
+ * @internal
34
+ */
35
+ export type JournalOperation = (typeof journalOperations)[number];
36
+ /**
37
+ * @internal
38
+ */
39
+ export interface JournalEntry {
40
+ path: string;
41
+ op: JournalOperation;
42
+ }
43
+ /**
44
+ * Tracks various operations for the CoW backend
45
+ * @internal
46
+ */
47
+ export declare class Journal {
48
+ readonly fs: CopyOnWriteFS;
49
+ entries: JournalEntry[];
50
+ constructor(fs: CopyOnWriteFS);
51
+ toString(): string;
15
52
  /**
16
- * The file system that initially populates this file system.
53
+ * Parse a journal from a string
17
54
  */
18
- readable: FileSystem;
55
+ fromString(value: string): void;
56
+ add(op: JournalOperation, path: string): void;
57
+ has(op: JournalOperation, path: string): boolean;
58
+ isDeleted(path: string): boolean;
19
59
  }
20
60
  /**
21
- * OverlayFS makes a read-only filesystem writable by storing writes on a second, writable file system.
22
- * Deletes are persisted via metadata stored on the writable file system.
23
- *
24
- * This class contains no locking whatsoever. It is mutexed to prevent races.
25
- *
61
+ * Using a readable file system as a base, writes are done to a writable file system.
26
62
  * @internal
27
63
  */
28
- export declare class OverlayFS extends FileSystem {
29
- ready(): Promise<void>;
30
- readonly writable: FileSystem;
64
+ export declare class CopyOnWriteFS extends FileSystem {
65
+ /**
66
+ * The file system that initially populates this file system.
67
+ */
31
68
  readonly readable: FileSystem;
32
- private _isInitialized;
33
- private _deletedFiles;
34
- private _deleteLog;
35
- private _deleteLogUpdatePending;
36
- private _deleteLogUpdateNeeded;
37
- private _deleteLogError?;
38
- private _ready;
39
- constructor({ writable, readable }: OverlayOptions);
69
+ /**
70
+ * The file system to write modified files to.
71
+ */
72
+ readonly writable: FileSystem;
73
+ ready(): Promise<void>;
74
+ readonly journal: Journal;
75
+ constructor(
76
+ /**
77
+ * The file system that initially populates this file system.
78
+ */
79
+ readable: FileSystem,
80
+ /**
81
+ * The file system to write modified files to.
82
+ */
83
+ writable: FileSystem);
84
+ isDeleted(path: string): boolean;
40
85
  /**
41
86
  * @todo Consider trying to track information on the writable as well
42
87
  */
@@ -47,13 +92,6 @@ export declare class OverlayFS extends FileSystem {
47
92
  readSync(path: string, buffer: Uint8Array, offset: number, end: number): void;
48
93
  write(path: string, buffer: Uint8Array, offset: number): Promise<void>;
49
94
  writeSync(path: string, buffer: Uint8Array, offset: number): void;
50
- /**
51
- * Called once to load up metadata stored on the writable file system.
52
- * @internal
53
- */
54
- _initialize(): Promise<void>;
55
- getDeletionLog(): string;
56
- restoreDeletionLog(log: string): Promise<void>;
57
95
  rename(oldPath: string, newPath: string): Promise<void>;
58
96
  renameSync(oldPath: string, newPath: string): void;
59
97
  stat(path: string): Promise<Stats>;
@@ -72,11 +110,6 @@ export declare class OverlayFS extends FileSystem {
72
110
  mkdirSync(path: string, mode: number, options: CreationOptions): void;
73
111
  readdir(path: string): Promise<string[]>;
74
112
  readdirSync(path: string): string[];
75
- private deletePath;
76
- private updateLog;
77
- private _reparseDeletionLog;
78
- private checkInitialized;
79
- private checkPath;
80
113
  /**
81
114
  * Create the needed parent directories on the writable storage should they not exist.
82
115
  * Use modes from the read-only storage.
@@ -101,8 +134,13 @@ export declare class OverlayFS extends FileSystem {
101
134
  private copyToWritableSync;
102
135
  private copyToWritable;
103
136
  }
104
- declare const _Overlay: {
105
- readonly name: "Overlay";
137
+ /**
138
+ * @hidden @deprecated use `CopyOnWriteFS`
139
+ */
140
+ export declare class OverlayFS extends CopyOnWriteFS {
141
+ }
142
+ declare const _CopyOnWrite: {
143
+ readonly name: "CopyOnWrite";
106
144
  readonly options: {
107
145
  readonly writable: {
108
146
  readonly type: "object";
@@ -112,11 +150,15 @@ declare const _Overlay: {
112
150
  readonly type: "object";
113
151
  readonly required: true;
114
152
  };
153
+ readonly journal: {
154
+ readonly type: "object";
155
+ readonly required: false;
156
+ };
115
157
  };
116
- readonly create: (options: OverlayOptions) => OverlayFS;
158
+ readonly create: (options: CopyOnWriteOptions) => CopyOnWriteFS;
117
159
  };
118
- type _Overlay = typeof _Overlay;
119
- export interface Overlay extends _Overlay {
160
+ type _CopyOnWrite = typeof _CopyOnWrite;
161
+ export interface CopyOnWrite extends _CopyOnWrite {
120
162
  }
121
163
  /**
122
164
  * Overlay makes a read-only filesystem writable by storing writes on a second, writable file system.
@@ -124,5 +166,13 @@ export interface Overlay extends _Overlay {
124
166
  * @category Backends and Configuration
125
167
  * @internal
126
168
  */
169
+ export declare const CopyOnWrite: CopyOnWrite;
170
+ export interface Overlay extends _CopyOnWrite {
171
+ }
172
+ /**
173
+ * @deprecated Use `CopyOnWrite`
174
+ * @category Backends and Configuration
175
+ * @internal @hidden
176
+ */
127
177
  export declare const Overlay: Overlay;
128
178
  export {};
@@ -52,43 +52,98 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
52
52
  });
53
53
  import { canary } from 'utilium';
54
54
  import { Errno, ErrnoError } from '../internal/error.js';
55
- import { LazyFile, parseFlag } from '../internal/file.js';
55
+ import { LazyFile } from '../internal/file.js';
56
56
  import { FileSystem } from '../internal/filesystem.js';
57
- import { crit, err, info } from '../internal/log.js';
58
- import { decodeUTF8, encodeUTF8 } from '../utils.js';
57
+ import { debug, err, warn } from '../internal/log.js';
59
58
  import { dirname, join } from '../vfs/path.js';
60
- /** @internal */
61
- const deletionLogPath = '/.deleted';
59
+ const journalOperations = ['delete'];
60
+ /** Because TS doesn't work right w/o it */
61
+ function isJournalOp(op) {
62
+ return journalOperations.includes(op);
63
+ }
64
+ const maxOpLength = Math.max(...journalOperations.map(op => op.length));
65
+ const journalMagicString = '#journal@v0\n';
62
66
  /**
63
- * OverlayFS makes a read-only filesystem writable by storing writes on a second, writable file system.
64
- * Deletes are persisted via metadata stored on the writable file system.
65
- *
66
- * This class contains no locking whatsoever. It is mutexed to prevent races.
67
- *
67
+ * Tracks various operations for the CoW backend
68
+ * @internal
69
+ */
70
+ export class Journal {
71
+ constructor(fs) {
72
+ this.fs = fs;
73
+ this.entries = [];
74
+ }
75
+ toString() {
76
+ return journalMagicString + this.entries.map(entry => `${entry.op.padEnd(maxOpLength)} ${entry.path}`).join('\n');
77
+ }
78
+ /**
79
+ * Parse a journal from a string
80
+ */
81
+ fromString(value) {
82
+ if (!value.startsWith(journalMagicString))
83
+ throw err(new ErrnoError(Errno.EINVAL, 'Invalid journal contents, refusing to parse'));
84
+ for (const line of value.split('\n')) {
85
+ if (line.startsWith('#'))
86
+ continue; // ignore comments
87
+ const [op, path] = line.split(/\s+/);
88
+ if (!isJournalOp(op)) {
89
+ warn('Unknown operation in journal (skipping): ' + op);
90
+ continue;
91
+ }
92
+ this.entries.push({ op, path });
93
+ }
94
+ }
95
+ add(op, path) {
96
+ this.entries.push({ op, path });
97
+ }
98
+ has(op, path) {
99
+ const test = JSON.stringify({ op, path });
100
+ for (const entry of this.entries)
101
+ if (JSON.stringify(entry) === test)
102
+ return true;
103
+ return false;
104
+ }
105
+ isDeleted(path) {
106
+ let deleted = false;
107
+ for (const entry of this.entries) {
108
+ if (entry.path != path)
109
+ continue;
110
+ switch (entry.op) {
111
+ case 'delete':
112
+ deleted = true;
113
+ }
114
+ }
115
+ return deleted;
116
+ }
117
+ }
118
+ /**
119
+ * Using a readable file system as a base, writes are done to a writable file system.
68
120
  * @internal
69
121
  */
70
- export class OverlayFS extends FileSystem {
122
+ export class CopyOnWriteFS extends FileSystem {
71
123
  async ready() {
72
124
  await this.readable.ready();
73
125
  await this.writable.ready();
74
- await this._ready;
75
126
  }
76
- constructor({ writable, readable }) {
127
+ constructor(
128
+ /**
129
+ * The file system that initially populates this file system.
130
+ */
131
+ readable,
132
+ /**
133
+ * The file system to write modified files to.
134
+ */
135
+ writable) {
77
136
  super(0x62756c6c, readable.name);
78
- this._isInitialized = false;
79
- this._deletedFiles = new Set();
80
- this._deleteLog = '';
81
- // If 'true', we have scheduled a delete log update.
82
- this._deleteLogUpdatePending = false;
83
- // If 'true', a delete log update is needed after the scheduled delete log
84
- // update finishes.
85
- this._deleteLogUpdateNeeded = false;
86
- this.writable = writable;
87
137
  this.readable = readable;
88
- if (this.writable.attributes.has('no_write')) {
138
+ this.writable = writable;
139
+ this.journal = new Journal(this);
140
+ if (writable.attributes.has('no_write')) {
89
141
  throw err(new ErrnoError(Errno.EINVAL, 'Writable file system can not be written to'));
90
142
  }
91
- this._ready = this._initialize();
143
+ readable.attributes.set('no_write');
144
+ }
145
+ isDeleted(path) {
146
+ return this.journal.isDeleted(path);
92
147
  }
93
148
  /**
94
149
  * @todo Consider trying to track information on the writable as well
@@ -120,74 +175,35 @@ export class OverlayFS extends FileSystem {
120
175
  this.copyForWriteSync(path);
121
176
  return this.writable.writeSync(path, buffer, offset);
122
177
  }
123
- /**
124
- * Called once to load up metadata stored on the writable file system.
125
- * @internal
126
- */
127
- async _initialize() {
128
- if (this._isInitialized) {
129
- return;
130
- }
131
- // Read deletion log, process into metadata.
132
- try {
133
- const file = await this.writable.openFile(deletionLogPath, parseFlag('r'));
134
- const { size } = await file.stat();
135
- const { buffer } = await file.read(new Uint8Array(size));
136
- this._deleteLog = decodeUTF8(buffer);
137
- }
138
- catch (error) {
139
- if (error.errno !== Errno.ENOENT)
140
- throw err(error);
141
- info('Overlay does not have a deletion log');
142
- }
143
- this._isInitialized = true;
144
- this._reparseDeletionLog();
145
- }
146
- getDeletionLog() {
147
- return this._deleteLog;
148
- }
149
- async restoreDeletionLog(log) {
150
- this._deleteLog = log;
151
- this._reparseDeletionLog();
152
- await this.updateLog('');
153
- }
154
178
  async rename(oldPath, newPath) {
155
- this.checkInitialized();
156
- this.checkPath(oldPath);
157
- this.checkPath(newPath);
158
179
  await this.copyForWrite(oldPath);
159
180
  try {
160
181
  await this.writable.rename(oldPath, newPath);
161
182
  }
162
183
  catch {
163
- if (this._deletedFiles.has(oldPath)) {
184
+ if (this.isDeleted(oldPath)) {
164
185
  throw ErrnoError.With('ENOENT', oldPath, 'rename');
165
186
  }
166
187
  }
167
188
  }
168
189
  renameSync(oldPath, newPath) {
169
- this.checkInitialized();
170
- this.checkPath(oldPath);
171
- this.checkPath(newPath);
172
190
  this.copyForWriteSync(oldPath);
173
191
  try {
174
192
  this.writable.renameSync(oldPath, newPath);
175
193
  }
176
194
  catch {
177
- if (this._deletedFiles.has(oldPath)) {
195
+ if (this.isDeleted(oldPath)) {
178
196
  throw ErrnoError.With('ENOENT', oldPath, 'rename');
179
197
  }
180
198
  }
181
199
  }
182
200
  async stat(path) {
183
- this.checkInitialized();
184
201
  try {
185
202
  return await this.writable.stat(path);
186
203
  }
187
204
  catch {
188
- if (this._deletedFiles.has(path)) {
205
+ if (this.isDeleted(path))
189
206
  throw ErrnoError.With('ENOENT', path, 'stat');
190
- }
191
207
  const oldStat = await this.readable.stat(path);
192
208
  // Make the oldStat's mode writable.
193
209
  oldStat.mode |= 0o222;
@@ -195,14 +211,12 @@ export class OverlayFS extends FileSystem {
195
211
  }
196
212
  }
197
213
  statSync(path) {
198
- this.checkInitialized();
199
214
  try {
200
215
  return this.writable.statSync(path);
201
216
  }
202
217
  catch {
203
- if (this._deletedFiles.has(path)) {
218
+ if (this.isDeleted(path))
204
219
  throw ErrnoError.With('ENOENT', path, 'stat');
205
- }
206
220
  const oldStat = this.readable.statSync(path);
207
221
  // Make the oldStat's mode writable.
208
222
  oldStat.mode |= 0o222;
@@ -224,28 +238,22 @@ export class OverlayFS extends FileSystem {
224
238
  return new LazyFile(this, path, flag, stats);
225
239
  }
226
240
  async createFile(path, flag, mode, options) {
227
- this.checkInitialized();
228
241
  await this.writable.createFile(path, flag, mode, options);
229
242
  return this.openFile(path, flag);
230
243
  }
231
244
  createFileSync(path, flag, mode, options) {
232
- this.checkInitialized();
233
245
  this.writable.createFileSync(path, flag, mode, options);
234
246
  return this.openFileSync(path, flag);
235
247
  }
236
248
  async link(srcpath, dstpath) {
237
- this.checkInitialized();
238
249
  await this.copyForWrite(srcpath);
239
250
  await this.writable.link(srcpath, dstpath);
240
251
  }
241
252
  linkSync(srcpath, dstpath) {
242
- this.checkInitialized();
243
253
  this.copyForWriteSync(srcpath);
244
254
  this.writable.linkSync(srcpath, dstpath);
245
255
  }
246
256
  async unlink(path) {
247
- this.checkInitialized();
248
- this.checkPath(path);
249
257
  if (!(await this.exists(path))) {
250
258
  throw ErrnoError.With('ENOENT', path, 'unlink');
251
259
  }
@@ -254,25 +262,21 @@ export class OverlayFS extends FileSystem {
254
262
  }
255
263
  // if it still exists add to the delete log
256
264
  if (await this.exists(path)) {
257
- await this.deletePath(path);
265
+ this.journal.add('delete', path);
258
266
  }
259
267
  }
260
268
  unlinkSync(path) {
261
- this.checkInitialized();
262
- this.checkPath(path);
263
- if (!this.existsSync(path)) {
269
+ if (!this.existsSync(path))
264
270
  throw ErrnoError.With('ENOENT', path, 'unlink');
265
- }
266
271
  if (this.writable.existsSync(path)) {
267
272
  this.writable.unlinkSync(path);
268
273
  }
269
274
  // if it still exists add to the delete log
270
275
  if (this.existsSync(path)) {
271
- void this.deletePath(path);
276
+ this.journal.add('delete', path);
272
277
  }
273
278
  }
274
279
  async rmdir(path) {
275
- this.checkInitialized();
276
280
  if (!(await this.exists(path))) {
277
281
  throw ErrnoError.With('ENOENT', path, 'rmdir');
278
282
  }
@@ -286,10 +290,9 @@ export class OverlayFS extends FileSystem {
286
290
  if ((await this.readdir(path)).length) {
287
291
  throw ErrnoError.With('ENOTEMPTY', path, 'rmdir');
288
292
  }
289
- await this.deletePath(path);
293
+ this.journal.add('delete', path);
290
294
  }
291
295
  rmdirSync(path) {
292
- this.checkInitialized();
293
296
  if (!this.existsSync(path)) {
294
297
  throw ErrnoError.With('ENOENT', path, 'rmdir');
295
298
  }
@@ -303,137 +306,55 @@ export class OverlayFS extends FileSystem {
303
306
  if (this.readdirSync(path).length) {
304
307
  throw ErrnoError.With('ENOTEMPTY', path, 'rmdir');
305
308
  }
306
- void this.deletePath(path);
309
+ this.journal.add('delete', path);
307
310
  }
308
311
  async mkdir(path, mode, options) {
309
- this.checkInitialized();
310
- if (await this.exists(path)) {
312
+ if (await this.exists(path))
311
313
  throw ErrnoError.With('EEXIST', path, 'mkdir');
312
- }
313
- // The below will throw should any of the parent directories fail to exist on _writable.
314
314
  await this.createParentDirectories(path);
315
315
  await this.writable.mkdir(path, mode, options);
316
316
  }
317
317
  mkdirSync(path, mode, options) {
318
- this.checkInitialized();
319
- if (this.existsSync(path)) {
318
+ if (this.existsSync(path))
320
319
  throw ErrnoError.With('EEXIST', path, 'mkdir');
321
- }
322
- // The below will throw should any of the parent directories fail to exist on _writable.
323
320
  this.createParentDirectoriesSync(path);
324
321
  this.writable.mkdirSync(path, mode, options);
325
322
  }
326
323
  async readdir(path) {
327
- this.checkInitialized();
328
- // Readdir in both, check delete log on RO file system's listing, merge, return.
329
- const contents = [];
330
- try {
331
- contents.push(...(await this.writable.readdir(path)));
332
- }
333
- catch {
334
- // NOP.
335
- }
336
- try {
337
- contents.push(...(await this.readable.readdir(path)).filter((fPath) => !this._deletedFiles.has(`${path}/${fPath}`)));
338
- }
339
- catch {
340
- // NOP.
341
- }
342
- const seenMap = {};
343
- return contents.filter((path) => {
344
- const result = !seenMap[path];
345
- seenMap[path] = true;
346
- return result;
347
- });
348
- }
349
- readdirSync(path) {
350
- this.checkInitialized();
351
- // Readdir in both, check delete log on RO file system's listing, merge, return.
352
- let contents = [];
353
- try {
354
- contents = contents.concat(this.writable.readdirSync(path));
355
- }
356
- catch {
357
- // NOP.
358
- }
359
- try {
360
- contents = contents.concat(this.readable.readdirSync(path).filter((fPath) => !this._deletedFiles.has(`${path}/${fPath}`)));
361
- }
362
- catch {
363
- // NOP.
364
- }
365
- const seenMap = {};
366
- return contents.filter((path) => {
367
- const result = !seenMap[path];
368
- seenMap[path] = true;
369
- return result;
370
- });
371
- }
372
- async deletePath(path) {
373
- this._deletedFiles.add(path);
374
- await this.updateLog(`d${path}\n`);
375
- }
376
- async updateLog(addition) {
377
- this._deleteLog += addition;
378
- if (this._deleteLogUpdatePending) {
379
- this._deleteLogUpdateNeeded = true;
380
- return;
381
- }
382
- this._deleteLogUpdatePending = true;
383
- const log = await this.writable.openFile(deletionLogPath, parseFlag('w'));
384
- try {
385
- await log.write(encodeUTF8(this._deleteLog));
386
- if (this._deleteLogUpdateNeeded) {
387
- this._deleteLogUpdateNeeded = false;
388
- await this.updateLog('');
324
+ if (this.isDeleted(path))
325
+ throw ErrnoError.With('ENOENT', path, 'readdir');
326
+ const entries = await this.writable.readdir(path);
327
+ if (await this.writable.exists(path))
328
+ for (const entry of await this.writable.readdir(path)) {
329
+ if (!entries.includes(entry))
330
+ entries.push(entry);
389
331
  }
390
- }
391
- catch (e) {
392
- this._deleteLogError = e;
393
- }
394
- finally {
395
- this._deleteLogUpdatePending = false;
396
- }
332
+ return entries.filter(entry => !this.isDeleted(join(path, entry)));
397
333
  }
398
- _reparseDeletionLog() {
399
- this._deletedFiles.clear();
400
- for (const entry of this._deleteLog.split('\n')) {
401
- if (!entry.startsWith('d')) {
402
- continue;
334
+ readdirSync(path) {
335
+ if (this.isDeleted(path))
336
+ throw ErrnoError.With('ENOENT', path, 'readdir');
337
+ const entries = this.writable.readdirSync(path);
338
+ if (this.writable.existsSync(path))
339
+ for (const entry of this.writable.readdirSync(path)) {
340
+ if (!entries.includes(entry))
341
+ entries.push(entry);
403
342
  }
404
- // If the log entry begins w/ 'd', it's a deletion.
405
- this._deletedFiles.add(entry.slice(1));
406
- }
407
- }
408
- checkInitialized() {
409
- if (!this._isInitialized) {
410
- throw crit(new ErrnoError(Errno.EPERM, 'Overlay is not initialized'), { fs: this });
411
- }
412
- if (!this._deleteLogError) {
413
- return;
414
- }
415
- const error = this._deleteLogError;
416
- delete this._deleteLogError;
417
- throw error;
418
- }
419
- checkPath(path) {
420
- if (path == deletionLogPath) {
421
- throw ErrnoError.With('EPERM', path, 'checkPath');
422
- }
343
+ return entries.filter(entry => !this.isDeleted(join(path, entry)));
423
344
  }
424
345
  /**
425
346
  * Create the needed parent directories on the writable storage should they not exist.
426
347
  * Use modes from the read-only storage.
427
348
  */
428
349
  createParentDirectoriesSync(path) {
429
- let parent = dirname(path);
430
350
  const toCreate = [];
431
351
  const silence = canary(ErrnoError.With('EDEADLK', path));
432
- while (!this.writable.existsSync(parent)) {
352
+ for (let parent = dirname(path); !this.writable.existsSync(parent); parent = dirname(parent)) {
433
353
  toCreate.push(parent);
434
- parent = dirname(parent);
435
354
  }
436
355
  silence();
356
+ if (toCreate.length)
357
+ debug('COW: Creating parent directories: ' + toCreate.join(', '));
437
358
  for (const path of toCreate.reverse()) {
438
359
  const { uid, gid, mode } = this.statSync(path);
439
360
  this.writable.mkdirSync(path, mode, { uid, gid });
@@ -444,14 +365,14 @@ export class OverlayFS extends FileSystem {
444
365
  * Use modes from the read-only storage.
445
366
  */
446
367
  async createParentDirectories(path) {
447
- let parent = dirname(path);
448
368
  const toCreate = [];
449
369
  const silence = canary(ErrnoError.With('EDEADLK', path));
450
- while (!(await this.writable.exists(parent))) {
370
+ for (let parent = dirname(path); !(await this.writable.exists(parent)); parent = dirname(parent)) {
451
371
  toCreate.push(parent);
452
- parent = dirname(parent);
453
372
  }
454
373
  silence();
374
+ if (toCreate.length)
375
+ debug('COW: Creating parent directories: ' + toCreate.join(', '));
455
376
  for (const path of toCreate.reverse()) {
456
377
  const { uid, gid, mode } = await this.stat(path);
457
378
  await this.writable.mkdir(path, mode, { uid, gid });
@@ -543,14 +464,27 @@ export class OverlayFS extends FileSystem {
543
464
  }
544
465
  }
545
466
  }
546
- const _Overlay = {
547
- name: 'Overlay',
467
+ /**
468
+ * @hidden @deprecated use `CopyOnWriteFS`
469
+ */
470
+ export class OverlayFS extends CopyOnWriteFS {
471
+ }
472
+ const _CopyOnWrite = {
473
+ name: 'CopyOnWrite',
548
474
  options: {
549
475
  writable: { type: 'object', required: true },
550
476
  readable: { type: 'object', required: true },
477
+ journal: { type: 'object', required: false },
551
478
  },
552
479
  create(options) {
553
- return new OverlayFS(options);
480
+ const fs = new CopyOnWriteFS(options.readable, options.writable);
481
+ if (typeof options.journal == 'string') {
482
+ fs.journal.fromString(options.journal);
483
+ }
484
+ if (Array.isArray(options.journal)) {
485
+ fs.journal.entries.push(...options.journal);
486
+ }
487
+ return fs;
554
488
  },
555
489
  };
556
490
  /**
@@ -559,4 +493,10 @@ const _Overlay = {
559
493
  * @category Backends and Configuration
560
494
  * @internal
561
495
  */
562
- export const Overlay = _Overlay;
496
+ export const CopyOnWrite = _CopyOnWrite;
497
+ /**
498
+ * @deprecated Use `CopyOnWrite`
499
+ * @category Backends and Configuration
500
+ * @internal @hidden
501
+ */
502
+ export const Overlay = _CopyOnWrite;