@zenfs/core 2.6.0 → 2.6.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.
- package/dist/backends/cow.d.ts +5 -0
- package/dist/backends/cow.js +73 -24
- package/dist/backends/fetch.d.ts +4 -0
- package/dist/backends/fetch.js +7 -3
- package/dist/node/promises.js +10 -2
- package/dist/node/sync.js +12 -6
- package/dist/vfs/async.js +29 -21
- package/dist/vfs/file.js +5 -5
- package/dist/vfs/shared.js +4 -3
- package/dist/vfs/sync.js +35 -21
- package/dist/vfs/vcache.js +2 -2
- package/dist/vfs/vnode.d.ts +6 -0
- package/dist/vfs/vnode.js +15 -0
- package/package.json +1 -1
- package/scripts/make-index.js +36 -15
- package/tests/backend/fetch.test.ts +24 -1
- package/tests/common/context.test.ts +8 -0
- package/tests/common.ts +0 -2
- package/tests/fs/concurrency.test.ts +61 -0
- package/tests/fs/directory.test.ts +21 -0
- package/tests/fs/links.test.ts +146 -0
- package/tests/native/setup.ts +1 -0
package/dist/backends/cow.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ export declare class Journal extends EventEmitter<{
|
|
|
46
46
|
fromString(value: string): this;
|
|
47
47
|
add(op: JournalOperation, path: string): void;
|
|
48
48
|
has(op: JournalOperation, path: string): boolean;
|
|
49
|
+
delete(op: JournalOperation, path: string): void;
|
|
49
50
|
isDeleted(path: string): boolean;
|
|
50
51
|
}
|
|
51
52
|
/**
|
|
@@ -127,11 +128,15 @@ export declare class CopyOnWriteFS extends FileSystem {
|
|
|
127
128
|
* Helper function:
|
|
128
129
|
* - Ensures p is on writable before proceeding. Throws an error if it doesn't exist.
|
|
129
130
|
* - Calls f to perform operation on writable.
|
|
131
|
+
*
|
|
132
|
+
* `keep` is the size the file is about to be truncated to, when the caller knows it.
|
|
133
|
+
* Bytes past it are discarded by the operation that follows, so they are not worth copying up.
|
|
130
134
|
*/
|
|
131
135
|
private copyForWriteSync;
|
|
132
136
|
private copyForWrite;
|
|
133
137
|
/**
|
|
134
138
|
* Copy from readable to writable storage.
|
|
139
|
+
* Only `[0, keep)` is copied; the caller is responsible for setting the final size.
|
|
135
140
|
* PRECONDITION: File does not exist on writable storage.
|
|
136
141
|
*/
|
|
137
142
|
private copyToWritableSync;
|
package/dist/backends/cow.js
CHANGED
|
@@ -53,6 +53,9 @@ export class Journal extends EventEmitter {
|
|
|
53
53
|
return true;
|
|
54
54
|
return false;
|
|
55
55
|
}
|
|
56
|
+
delete(op, path) {
|
|
57
|
+
this.entries = this.entries.filter(entry => entry.op != op || entry.path != path);
|
|
58
|
+
}
|
|
56
59
|
isDeleted(path) {
|
|
57
60
|
let deleted = false;
|
|
58
61
|
for (const entry of this.entries) {
|
|
@@ -168,6 +171,9 @@ export class CopyOnWriteFS extends FileSystem {
|
|
|
168
171
|
if (this.isDeleted(oldPath))
|
|
169
172
|
throw withErrno('ENOENT');
|
|
170
173
|
}
|
|
174
|
+
if (await this.readable.exists(oldPath))
|
|
175
|
+
this.journal.add('delete', oldPath);
|
|
176
|
+
this.journal.delete('delete', newPath);
|
|
171
177
|
this.renameInos(oldPath, newPath);
|
|
172
178
|
}
|
|
173
179
|
renameSync(oldPath, newPath) {
|
|
@@ -179,6 +185,9 @@ export class CopyOnWriteFS extends FileSystem {
|
|
|
179
185
|
if (this.isDeleted(oldPath))
|
|
180
186
|
throw withErrno('ENOENT');
|
|
181
187
|
}
|
|
188
|
+
if (this.readable.existsSync(oldPath))
|
|
189
|
+
this.journal.add('delete', oldPath);
|
|
190
|
+
this.journal.delete('delete', newPath);
|
|
182
191
|
this.renameInos(oldPath, newPath);
|
|
183
192
|
}
|
|
184
193
|
async stat(path) {
|
|
@@ -202,28 +211,34 @@ export class CopyOnWriteFS extends FileSystem {
|
|
|
202
211
|
}
|
|
203
212
|
}
|
|
204
213
|
async touch(path, metadata) {
|
|
205
|
-
await this.copyForWrite(path);
|
|
214
|
+
await this.copyForWrite(path, metadata.size);
|
|
206
215
|
await this.writable.touch(path, metadata);
|
|
207
216
|
}
|
|
208
217
|
touchSync(path, metadata) {
|
|
209
|
-
this.copyForWriteSync(path);
|
|
218
|
+
this.copyForWriteSync(path, metadata.size);
|
|
210
219
|
this.writable.touchSync(path, metadata);
|
|
211
220
|
}
|
|
212
221
|
async createFile(path, options) {
|
|
213
222
|
await this.createParentDirectories(path);
|
|
214
|
-
|
|
223
|
+
const inode = await this.writable.createFile(path, options);
|
|
224
|
+
this.journal.delete('delete', path);
|
|
225
|
+
return this.remapIno(path, inode);
|
|
215
226
|
}
|
|
216
227
|
createFileSync(path, options) {
|
|
217
228
|
this.createParentDirectoriesSync(path);
|
|
218
|
-
|
|
229
|
+
const inode = this.writable.createFileSync(path, options);
|
|
230
|
+
this.journal.delete('delete', path);
|
|
231
|
+
return this.remapIno(path, inode);
|
|
219
232
|
}
|
|
220
233
|
async link(srcpath, dstpath) {
|
|
221
234
|
await this.copyForWrite(srcpath);
|
|
222
235
|
await this.writable.link(srcpath, dstpath);
|
|
236
|
+
this.journal.delete('delete', dstpath);
|
|
223
237
|
}
|
|
224
238
|
linkSync(srcpath, dstpath) {
|
|
225
239
|
this.copyForWriteSync(srcpath);
|
|
226
240
|
this.writable.linkSync(srcpath, dstpath);
|
|
241
|
+
this.journal.delete('delete', dstpath);
|
|
227
242
|
}
|
|
228
243
|
async unlink(path) {
|
|
229
244
|
if (!(await this.exists(path)))
|
|
@@ -285,19 +300,27 @@ export class CopyOnWriteFS extends FileSystem {
|
|
|
285
300
|
if (await this.exists(path))
|
|
286
301
|
throw withErrno('EEXIST');
|
|
287
302
|
await this.createParentDirectories(path);
|
|
288
|
-
|
|
303
|
+
const inode = await this.writable.mkdir(path, options);
|
|
304
|
+
this.journal.delete('delete', path);
|
|
305
|
+
return this.remapIno(path, inode);
|
|
289
306
|
}
|
|
290
307
|
mkdirSync(path, options) {
|
|
291
308
|
if (this.existsSync(path))
|
|
292
309
|
throw withErrno('EEXIST');
|
|
293
310
|
this.createParentDirectoriesSync(path);
|
|
294
|
-
|
|
311
|
+
const inode = this.writable.mkdirSync(path, options);
|
|
312
|
+
this.journal.delete('delete', path);
|
|
313
|
+
return this.remapIno(path, inode);
|
|
295
314
|
}
|
|
296
315
|
async readdir(path) {
|
|
297
|
-
if (this.isDeleted(path)
|
|
316
|
+
if (this.isDeleted(path))
|
|
317
|
+
throw withErrno('ENOENT');
|
|
318
|
+
const readable = await this.readable.stat(path).catch(() => null);
|
|
319
|
+
const writable = await this.writable.stat(path).catch(() => null);
|
|
320
|
+
if (!readable && !writable)
|
|
298
321
|
throw withErrno('ENOENT');
|
|
299
|
-
const entries = (
|
|
300
|
-
if (
|
|
322
|
+
const entries = readable && isDirectory(readable) ? await this.readable.readdir(path) : [];
|
|
323
|
+
if (writable && isDirectory(writable))
|
|
301
324
|
for (const entry of await this.writable.readdir(path)) {
|
|
302
325
|
if (!entries.includes(entry))
|
|
303
326
|
entries.push(entry);
|
|
@@ -305,10 +328,26 @@ export class CopyOnWriteFS extends FileSystem {
|
|
|
305
328
|
return entries.filter(entry => !this.isDeleted(join(path, entry)));
|
|
306
329
|
}
|
|
307
330
|
readdirSync(path) {
|
|
308
|
-
if (this.isDeleted(path)
|
|
331
|
+
if (this.isDeleted(path))
|
|
332
|
+
throw withErrno('ENOENT');
|
|
333
|
+
let readable = null;
|
|
334
|
+
let writable = null;
|
|
335
|
+
try {
|
|
336
|
+
readable = this.readable.statSync(path);
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
// Not on the readable layer
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
writable = this.writable.statSync(path);
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// Not on the writable layer
|
|
346
|
+
}
|
|
347
|
+
if (!readable && !writable)
|
|
309
348
|
throw withErrno('ENOENT');
|
|
310
|
-
const entries =
|
|
311
|
-
if (
|
|
349
|
+
const entries = readable && isDirectory(readable) ? this.readable.readdirSync(path) : [];
|
|
350
|
+
if (writable && isDirectory(writable))
|
|
312
351
|
for (const entry of this.writable.readdirSync(path)) {
|
|
313
352
|
if (!entries.includes(entry))
|
|
314
353
|
entries.push(entry);
|
|
@@ -360,32 +399,36 @@ export class CopyOnWriteFS extends FileSystem {
|
|
|
360
399
|
* Helper function:
|
|
361
400
|
* - Ensures p is on writable before proceeding. Throws an error if it doesn't exist.
|
|
362
401
|
* - Calls f to perform operation on writable.
|
|
402
|
+
*
|
|
403
|
+
* `keep` is the size the file is about to be truncated to, when the caller knows it.
|
|
404
|
+
* Bytes past it are discarded by the operation that follows, so they are not worth copying up.
|
|
363
405
|
*/
|
|
364
|
-
copyForWriteSync(path) {
|
|
406
|
+
copyForWriteSync(path, keep) {
|
|
365
407
|
if (!this.existsSync(path))
|
|
366
408
|
throw withErrno('ENOENT');
|
|
367
409
|
if (!this.writable.existsSync(dirname(path))) {
|
|
368
410
|
this.createParentDirectoriesSync(path);
|
|
369
411
|
}
|
|
370
412
|
if (!this.writable.existsSync(path)) {
|
|
371
|
-
this.copyToWritableSync(path);
|
|
413
|
+
this.copyToWritableSync(path, keep);
|
|
372
414
|
}
|
|
373
415
|
}
|
|
374
|
-
async copyForWrite(path) {
|
|
416
|
+
async copyForWrite(path, keep) {
|
|
375
417
|
if (!(await this.exists(path)))
|
|
376
418
|
throw withErrno('ENOENT');
|
|
377
419
|
if (!(await this.writable.exists(dirname(path)))) {
|
|
378
420
|
await this.createParentDirectories(path);
|
|
379
421
|
}
|
|
380
422
|
if (!(await this.writable.exists(path))) {
|
|
381
|
-
return this.copyToWritable(path);
|
|
423
|
+
return this.copyToWritable(path, keep);
|
|
382
424
|
}
|
|
383
425
|
}
|
|
384
426
|
/**
|
|
385
427
|
* Copy from readable to writable storage.
|
|
428
|
+
* Only `[0, keep)` is copied; the caller is responsible for setting the final size.
|
|
386
429
|
* PRECONDITION: File does not exist on writable storage.
|
|
387
430
|
*/
|
|
388
|
-
copyToWritableSync(path) {
|
|
431
|
+
copyToWritableSync(path, keep) {
|
|
389
432
|
const stats = this.readable.statSync(path);
|
|
390
433
|
if (isDirectory(stats)) {
|
|
391
434
|
this.writable.mkdirSync(path, stats);
|
|
@@ -394,13 +437,16 @@ export class CopyOnWriteFS extends FileSystem {
|
|
|
394
437
|
}
|
|
395
438
|
return;
|
|
396
439
|
}
|
|
397
|
-
const
|
|
398
|
-
|
|
440
|
+
const size = Math.min(stats.size, keep ?? stats.size);
|
|
441
|
+
const data = new Uint8Array(size);
|
|
442
|
+
if (size)
|
|
443
|
+
this.readable.readSync(path, data, 0, size);
|
|
399
444
|
this.writable.createFileSync(path, stats);
|
|
400
445
|
this.writable.touchSync(path, stats);
|
|
401
|
-
|
|
446
|
+
if (size)
|
|
447
|
+
this.writable.writeSync(path, data, 0);
|
|
402
448
|
}
|
|
403
|
-
async copyToWritable(path) {
|
|
449
|
+
async copyToWritable(path, keep) {
|
|
404
450
|
const stats = await this.readable.stat(path);
|
|
405
451
|
if (isDirectory(stats)) {
|
|
406
452
|
await this.writable.mkdir(path, stats);
|
|
@@ -409,11 +455,14 @@ export class CopyOnWriteFS extends FileSystem {
|
|
|
409
455
|
}
|
|
410
456
|
return;
|
|
411
457
|
}
|
|
412
|
-
const
|
|
413
|
-
|
|
458
|
+
const size = Math.min(stats.size, keep ?? stats.size);
|
|
459
|
+
const data = new Uint8Array(size);
|
|
460
|
+
if (size)
|
|
461
|
+
await this.readable.read(path, data, 0, size);
|
|
414
462
|
await this.writable.createFile(path, stats);
|
|
415
463
|
await this.writable.touch(path, stats);
|
|
416
|
-
|
|
464
|
+
if (size)
|
|
465
|
+
await this.writable.write(path, data, 0);
|
|
417
466
|
}
|
|
418
467
|
}
|
|
419
468
|
const _CopyOnWrite = {
|
package/dist/backends/fetch.d.ts
CHANGED
|
@@ -39,6 +39,10 @@ export declare class FetchFS extends IndexFS {
|
|
|
39
39
|
* @internal @hidden
|
|
40
40
|
*/
|
|
41
41
|
_asyncDone: Promise<unknown>;
|
|
42
|
+
/**
|
|
43
|
+
* Queue an async operation started from a sync method.
|
|
44
|
+
* Failures aren't rethrown to prevent further async ops from failing
|
|
45
|
+
*/
|
|
42
46
|
protected _async(p: Promise<unknown>): void;
|
|
43
47
|
constructor(index: Index, baseUrl: string, requestInit?: RequestInit, remoteWrite?: boolean | undefined);
|
|
44
48
|
protected remove(path: string): Promise<void>;
|
package/dist/backends/fetch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
2
|
import { withErrno } from 'kerium';
|
|
3
|
-
import { err, warn } from 'kerium/log';
|
|
3
|
+
import { debug, err, warn } from 'kerium/log';
|
|
4
4
|
import { decodeUTF8 } from 'utilium';
|
|
5
5
|
import * as requests from 'utilium/requests';
|
|
6
6
|
import { Index } from '../internal/file_index.js';
|
|
@@ -35,8 +35,12 @@ export class FetchFS extends IndexFS {
|
|
|
35
35
|
* @internal @hidden
|
|
36
36
|
*/
|
|
37
37
|
_asyncDone = Promise.resolve();
|
|
38
|
+
/**
|
|
39
|
+
* Queue an async operation started from a sync method.
|
|
40
|
+
* Failures aren't rethrown to prevent further async ops from failing
|
|
41
|
+
*/
|
|
38
42
|
_async(p) {
|
|
39
|
-
this._asyncDone = this._asyncDone.then(() => p);
|
|
43
|
+
this._asyncDone = this._asyncDone.then(() => p).catch(e => debug('Async operation failed: ' + (e.message ?? e)));
|
|
40
44
|
}
|
|
41
45
|
constructor(index, baseUrl, requestInit = {}, remoteWrite) {
|
|
42
46
|
super(0x206e6673, 'nfs', index);
|
|
@@ -74,7 +78,7 @@ export class FetchFS extends IndexFS {
|
|
|
74
78
|
if (!data)
|
|
75
79
|
throw withErrno('ENODATA');
|
|
76
80
|
if (missing.length) {
|
|
77
|
-
this._async(requests.get(this.baseUrl + path, { start: offset, end, size: inode.size, warn }));
|
|
81
|
+
this._async(requests.get(this.baseUrl + path, { start: offset, end, size: inode.size, warn }, this.requestInit));
|
|
78
82
|
throw withErrno('EAGAIN');
|
|
79
83
|
}
|
|
80
84
|
buffer.set(data);
|
package/dist/node/promises.js
CHANGED
|
@@ -55,7 +55,7 @@ import { Exception, rethrow, UV } from 'kerium';
|
|
|
55
55
|
import { encodeUTF8 } from 'utilium';
|
|
56
56
|
import * as constants from '../constants.js';
|
|
57
57
|
import { hasAccess, InodeFlags, isDirectory } from '../internal/inode.js';
|
|
58
|
-
import { dirname, join, matchesGlob } from '../path.js';
|
|
58
|
+
import { dirname, join, matchesGlob, resolve } from '../path.js';
|
|
59
59
|
import '../polyfills.js';
|
|
60
60
|
import { _tempDirName, globToRegex, normalizeMode, normalizeOptions, normalizePath, normalizeTime, validateFD } from '../utils.js';
|
|
61
61
|
import * as _async from '../vfs/async.js';
|
|
@@ -1081,8 +1081,16 @@ export async function cp(source, destination, opts) {
|
|
|
1081
1081
|
await Promise.all(entries.map(_cp));
|
|
1082
1082
|
break;
|
|
1083
1083
|
}
|
|
1084
|
-
case constants.S_IFREG:
|
|
1085
1084
|
case constants.S_IFLNK:
|
|
1085
|
+
if (opts?.dereference) {
|
|
1086
|
+
await copyFile.call(this, source, destination);
|
|
1087
|
+
}
|
|
1088
|
+
else {
|
|
1089
|
+
const link = await readlink.call(this, source);
|
|
1090
|
+
await symlink.call(this, opts?.verbatimSymlinks ? link : resolve.call(this, dirname(source), link), destination);
|
|
1091
|
+
}
|
|
1092
|
+
break;
|
|
1093
|
+
case constants.S_IFREG:
|
|
1086
1094
|
await copyFile.call(this, source, destination);
|
|
1087
1095
|
break;
|
|
1088
1096
|
case constants.S_IFBLK:
|
package/dist/node/sync.js
CHANGED
|
@@ -56,7 +56,7 @@ import { encodeUTF8 } from 'utilium';
|
|
|
56
56
|
import * as constants from '../constants.js';
|
|
57
57
|
import { wrap } from '../internal/error.js';
|
|
58
58
|
import { hasAccess, isDirectory } from '../internal/inode.js';
|
|
59
|
-
import { dirname, join, matchesGlob } from '../path.js';
|
|
59
|
+
import { dirname, join, matchesGlob, resolve } from '../path.js';
|
|
60
60
|
import { _tempDirName, globToRegex, normalizeMode, normalizeOptions, normalizePath, normalizeTime } from '../utils.js';
|
|
61
61
|
import { checkAccess } from '../vfs/config.js';
|
|
62
62
|
import { deleteFD, fromFD, toFD } from '../vfs/file.js';
|
|
@@ -202,7 +202,6 @@ export function writeFileSync(path, data, _options = {}) {
|
|
|
202
202
|
: _sync.open.call(this, path.toString(), {
|
|
203
203
|
flag,
|
|
204
204
|
mode: options.mode,
|
|
205
|
-
preserveSymlinks: true,
|
|
206
205
|
}), false);
|
|
207
206
|
file.writeSync(encodedData, 0, encodedData.byteLength, 0);
|
|
208
207
|
emitChange(this, 'change', path.toString());
|
|
@@ -237,7 +236,6 @@ export function appendFileSync(filename, data, _options = {}) {
|
|
|
237
236
|
: _sync.open.call(this, normalizePath(filename), {
|
|
238
237
|
flag,
|
|
239
238
|
mode: options.mode,
|
|
240
|
-
preserveSymlinks: true,
|
|
241
239
|
});
|
|
242
240
|
file.writeSync(encodedData, 0, encodedData.byteLength);
|
|
243
241
|
if (typeof file != 'number')
|
|
@@ -567,9 +565,9 @@ export function mkdtempDisposableSync(prefix, options) {
|
|
|
567
565
|
export function copyFileSync(source, destination, flags) {
|
|
568
566
|
source = normalizePath(source);
|
|
569
567
|
destination = normalizePath(destination);
|
|
570
|
-
if (flags && flags & constants.COPYFILE_EXCL && existsSync(destination))
|
|
568
|
+
if (flags && flags & constants.COPYFILE_EXCL && existsSync.call(this, destination))
|
|
571
569
|
throw UV('EEXIST', 'copyFile', destination);
|
|
572
|
-
writeFileSync.call(this, destination, readFileSync(source));
|
|
570
|
+
writeFileSync.call(this, destination, readFileSync.call(this, source));
|
|
573
571
|
emitChange(this, 'rename', destination.toString());
|
|
574
572
|
}
|
|
575
573
|
copyFileSync;
|
|
@@ -647,8 +645,16 @@ export function cpSync(source, destination, opts) {
|
|
|
647
645
|
cpSync.call(this, join(source, dirent.name), join(destination, dirent.name), opts);
|
|
648
646
|
}
|
|
649
647
|
break;
|
|
650
|
-
case constants.S_IFREG:
|
|
651
648
|
case constants.S_IFLNK:
|
|
649
|
+
if (opts?.dereference) {
|
|
650
|
+
copyFileSync.call(this, source, destination);
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
const link = readlinkSync.call(this, source, 'utf8');
|
|
654
|
+
symlinkSync.call(this, opts?.verbatimSymlinks ? link : resolve.call(this, dirname(source), link), destination);
|
|
655
|
+
}
|
|
656
|
+
break;
|
|
657
|
+
case constants.S_IFREG:
|
|
652
658
|
copyFileSync.call(this, source, destination);
|
|
653
659
|
break;
|
|
654
660
|
case constants.S_IFBLK:
|
package/dist/vfs/async.js
CHANGED
|
@@ -102,7 +102,7 @@ export async function resolve($, path, preserveSymlinks, extra) {
|
|
|
102
102
|
throw setUVMessage(Object.assign(e, { syscall: 'stat', path: maybePath, ...extra }));
|
|
103
103
|
}));
|
|
104
104
|
if (!stats)
|
|
105
|
-
return { ...resolved, fullPath:
|
|
105
|
+
return { ...resolved, fullPath: maybePath };
|
|
106
106
|
if (!isSymbolicLink(stats)) {
|
|
107
107
|
return { ...resolved, fullPath: maybePath, stats };
|
|
108
108
|
}
|
|
@@ -184,16 +184,36 @@ export async function readlink(path) {
|
|
|
184
184
|
return decodeUTF8(data);
|
|
185
185
|
}
|
|
186
186
|
export async function mkdir(path, options = {}) {
|
|
187
|
-
|
|
187
|
+
const original = normalizePath(path);
|
|
188
188
|
const { euid: uid, egid: gid } = contextOf(this).credentials;
|
|
189
189
|
const { mode = 0o777, recursive } = options;
|
|
190
|
-
const
|
|
191
|
-
const
|
|
190
|
+
const $ex = { syscall: 'mkdir', path: original };
|
|
191
|
+
const { fullPath: realParent } = await resolve(this, dirname(original), false, $ex);
|
|
192
|
+
const { fs, path: target } = resolveMount(join(realParent, basename(original)), this, $ex);
|
|
193
|
+
const followed = realParent != dirname(original);
|
|
194
|
+
let firstCreated;
|
|
195
|
+
const __create = async (path, resolved) => {
|
|
192
196
|
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
193
197
|
try {
|
|
198
|
+
const parentPath = dirname(resolved);
|
|
199
|
+
const parent = recursive && parentPath != '/' ? await __create(dirname(path), parentPath) : await fs.stat(parentPath);
|
|
200
|
+
const _ = __addDisposableResource(env_2, await lockPath(fs, parentPath, 'rw', parent), false);
|
|
201
|
+
if (recursive) {
|
|
202
|
+
const existing = cacheOf(fs).get(resolved)?.inode ?? (await fs.stat(resolved).catch(() => null));
|
|
203
|
+
if (existing) {
|
|
204
|
+
const stats = isSymbolicLink(existing) ? (await resolve(this, path, false, $ex)).stats : existing;
|
|
205
|
+
if (!stats)
|
|
206
|
+
throw UV('ENOENT', $ex);
|
|
207
|
+
if (!isDirectory(stats))
|
|
208
|
+
throw UV(resolved == target ? 'EEXIST' : 'ENOTDIR', $ex);
|
|
209
|
+
return existing;
|
|
210
|
+
}
|
|
211
|
+
// Node reports the link itself with ENOTDIR here, while `mkdirSync` reports the requested path with ENOENT
|
|
212
|
+
if (followed && (await fs.exists(join(parentPath, basename(path)))))
|
|
213
|
+
throw UV('ENOTDIR', 'mkdir', path);
|
|
214
|
+
}
|
|
194
215
|
if (checkAccess && !hasAccess(this, parent, constants.W_OK))
|
|
195
216
|
throw UV('EACCES', 'mkdir', path);
|
|
196
|
-
const _ = __addDisposableResource(env_2, await lockPath(fs, dirname(resolved), 'rw', parent), false);
|
|
197
217
|
const inode = await fs
|
|
198
218
|
.mkdir(resolved, {
|
|
199
219
|
mode,
|
|
@@ -201,6 +221,8 @@ export async function mkdir(path, options = {}) {
|
|
|
201
221
|
gid: parent.mode & constants.S_ISGID ? parent.gid : gid,
|
|
202
222
|
})
|
|
203
223
|
.catch(rethrow({ syscall: 'mkdir', path }));
|
|
224
|
+
if (recursive)
|
|
225
|
+
firstCreated ??= path;
|
|
204
226
|
emitChange(this, 'rename', path);
|
|
205
227
|
return inode;
|
|
206
228
|
}
|
|
@@ -212,22 +234,8 @@ export async function mkdir(path, options = {}) {
|
|
|
212
234
|
__disposeResources(env_2);
|
|
213
235
|
}
|
|
214
236
|
};
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
const dirs = [];
|
|
220
|
-
let origDir = path;
|
|
221
|
-
for (let dir = resolved; !(await fs.exists(dir)); dir = dirname(dir), origDir = dirname(origDir)) {
|
|
222
|
-
dirs.unshift([origDir, dir]);
|
|
223
|
-
}
|
|
224
|
-
if (!dirs.length)
|
|
225
|
-
return;
|
|
226
|
-
const stats = [await fs.stat(dirname(dirs[0][1]))];
|
|
227
|
-
for (const [i, [path, resolved]] of dirs.entries()) {
|
|
228
|
-
stats.push(await __create(path, resolved, stats[i]));
|
|
229
|
-
}
|
|
230
|
-
return dirs[0][0];
|
|
237
|
+
await __create(original, target);
|
|
238
|
+
return firstCreated;
|
|
231
239
|
}
|
|
232
240
|
export async function readdir(path, options = {}) {
|
|
233
241
|
path = normalizePath(path);
|
package/dist/vfs/file.js
CHANGED
|
@@ -178,7 +178,7 @@ let Handle = (() => {
|
|
|
178
178
|
throw UV('EBADF', 'sync', this.path);
|
|
179
179
|
if (!this.dirty)
|
|
180
180
|
return;
|
|
181
|
-
const _ = __addDisposableResource(env_1, this.vnode.lockSync('
|
|
181
|
+
const _ = __addDisposableResource(env_1, this.vnode.lockSync('rw'), false);
|
|
182
182
|
this.vnode.syncSync();
|
|
183
183
|
}
|
|
184
184
|
catch (e_1) {
|
|
@@ -199,7 +199,7 @@ let Handle = (() => {
|
|
|
199
199
|
if (this.closed)
|
|
200
200
|
throw UV('EBADF', 'close', this.path);
|
|
201
201
|
this.syncSync();
|
|
202
|
-
this.disposeSync();
|
|
202
|
+
this.disposeSync(true);
|
|
203
203
|
}
|
|
204
204
|
/**
|
|
205
205
|
* Cleans up. This will *not* sync the file data to the FS
|
|
@@ -385,7 +385,7 @@ let Handle = (() => {
|
|
|
385
385
|
throw UV('EBADF', 'sync', this.path);
|
|
386
386
|
if (!this.dirty)
|
|
387
387
|
return;
|
|
388
|
-
const _ = __addDisposableResource(env_8, await this.vnode.lock('
|
|
388
|
+
const _ = __addDisposableResource(env_8, await this.vnode.lock('rw'), false);
|
|
389
389
|
await this.vnode.sync();
|
|
390
390
|
}
|
|
391
391
|
catch (e_8) {
|
|
@@ -406,7 +406,7 @@ let Handle = (() => {
|
|
|
406
406
|
if (this.closed)
|
|
407
407
|
throw UV('EBADF', 'close', this.path);
|
|
408
408
|
await this.sync();
|
|
409
|
-
this.dispose();
|
|
409
|
+
this.dispose(true);
|
|
410
410
|
}
|
|
411
411
|
/**
|
|
412
412
|
* Cleans up. This will *not* sync the file data to the FS
|
|
@@ -653,7 +653,7 @@ let Handle = (() => {
|
|
|
653
653
|
async close() {
|
|
654
654
|
const env_17 = { stack: [], error: void 0, hasError: false };
|
|
655
655
|
try {
|
|
656
|
-
const _ = __addDisposableResource(env_17, await vnode.lock('
|
|
656
|
+
const _ = __addDisposableResource(env_17, await vnode.lock('rw'), false);
|
|
657
657
|
await vnode.sync();
|
|
658
658
|
}
|
|
659
659
|
catch (e_17) {
|
package/dist/vfs/shared.js
CHANGED
|
@@ -52,17 +52,18 @@ export function umount(mountPoint) {
|
|
|
52
52
|
}
|
|
53
53
|
const _done = () => {
|
|
54
54
|
caches.delete(fs.uuid);
|
|
55
|
-
|
|
56
|
-
notice('Unmounted ' + mountPoint);
|
|
55
|
+
info('Synced all changes to ' + mountPoint);
|
|
57
56
|
};
|
|
58
57
|
try {
|
|
59
58
|
caches.get(fs.uuid)?.syncSync();
|
|
60
59
|
_done();
|
|
61
60
|
}
|
|
62
61
|
catch {
|
|
63
|
-
warn(`Could not flush to ${mountPoint} synchronously
|
|
62
|
+
warn(`Could not flush to ${mountPoint} synchronously.`);
|
|
64
63
|
void caches.get(fs.uuid)?.sync().then(_done);
|
|
65
64
|
}
|
|
65
|
+
mounts.delete(mountPoint);
|
|
66
|
+
notice('Unmounted ' + mountPoint);
|
|
66
67
|
}
|
|
67
68
|
/**
|
|
68
69
|
* Gets the internal `FileSystem` for the path, then returns it along with the path relative to the FS' root
|
package/dist/vfs/sync.js
CHANGED
|
@@ -100,7 +100,7 @@ export function resolve($, path, preserveSymlinks, extra) {
|
|
|
100
100
|
}
|
|
101
101
|
catch (e) {
|
|
102
102
|
if (e.code === 'ENOENT')
|
|
103
|
-
return { ...resolved, fullPath:
|
|
103
|
+
return { ...resolved, fullPath: maybePath };
|
|
104
104
|
throw setUVMessage(Object.assign(e, { syscall: 'stat', path: maybePath, ...extra }));
|
|
105
105
|
}
|
|
106
106
|
if (!isSymbolicLink(stats)) {
|
|
@@ -189,21 +189,48 @@ export function readlink(path) {
|
|
|
189
189
|
return decodeUTF8(data);
|
|
190
190
|
}
|
|
191
191
|
export function mkdir(path, options = {}) {
|
|
192
|
-
|
|
193
|
-
const {
|
|
192
|
+
const original = normalizePath(path);
|
|
193
|
+
const $ex = { syscall: 'mkdir', path: original };
|
|
194
|
+
const { fullPath: realParent } = resolve(this, dirname(original), false, $ex);
|
|
195
|
+
const { fs, path: target } = resolveMount(join(realParent, basename(original)), this, $ex);
|
|
196
|
+
const followed = realParent != dirname(original);
|
|
194
197
|
const { euid: uid, egid: gid } = contextOf(this).credentials;
|
|
195
198
|
const { mode = 0o777, recursive } = options;
|
|
196
|
-
|
|
199
|
+
let firstCreated;
|
|
200
|
+
const __create = (path, resolved) => {
|
|
197
201
|
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
198
202
|
try {
|
|
203
|
+
const parentPath = dirname(resolved);
|
|
204
|
+
const parent = recursive && parentPath != '/' ? __create(dirname(path), parentPath) : fs.statSync(parentPath);
|
|
205
|
+
if (recursive) {
|
|
206
|
+
let existing;
|
|
207
|
+
try {
|
|
208
|
+
existing = cacheOf(fs).get(resolved)?.inode ?? fs.statSync(resolved);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// It doesn't exist yet
|
|
212
|
+
}
|
|
213
|
+
if (existing) {
|
|
214
|
+
const stats = isSymbolicLink(existing) ? resolve(this, path, false, $ex).stats : existing;
|
|
215
|
+
if (!stats)
|
|
216
|
+
throw UV('ENOENT', $ex);
|
|
217
|
+
if (!isDirectory(stats))
|
|
218
|
+
throw UV(resolved == target ? 'EEXIST' : 'ENOTDIR', $ex);
|
|
219
|
+
return existing;
|
|
220
|
+
}
|
|
221
|
+
if (followed && fs.existsSync(join(parentPath, basename(path))))
|
|
222
|
+
throw UV('ENOENT', $ex);
|
|
223
|
+
}
|
|
199
224
|
if (checkAccess && !hasAccess(this, parent, constants.W_OK))
|
|
200
|
-
throw UV('EACCES', 'mkdir',
|
|
201
|
-
const _ = __addDisposableResource(env_2, lockPathSync(fs,
|
|
225
|
+
throw UV('EACCES', 'mkdir', path);
|
|
226
|
+
const _ = __addDisposableResource(env_2, lockPathSync(fs, parentPath, 'rw', parent), false);
|
|
202
227
|
const inode = wrap(fs, 'mkdirSync', { path, syscall: 'mkdir' })(resolved, {
|
|
203
228
|
mode,
|
|
204
229
|
uid: parent.mode & constants.S_ISUID ? parent.uid : uid,
|
|
205
230
|
gid: parent.mode & constants.S_ISGID ? parent.gid : gid,
|
|
206
231
|
});
|
|
232
|
+
if (recursive)
|
|
233
|
+
firstCreated ??= path;
|
|
207
234
|
emitChange(this, 'rename', path);
|
|
208
235
|
return inode;
|
|
209
236
|
}
|
|
@@ -215,21 +242,8 @@ export function mkdir(path, options = {}) {
|
|
|
215
242
|
__disposeResources(env_2);
|
|
216
243
|
}
|
|
217
244
|
};
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
const dirs = [];
|
|
223
|
-
for (let dir = resolved, original = path; !fs.existsSync(dir); dir = dirname(dir), original = dirname(original)) {
|
|
224
|
-
dirs.unshift({ resolved: dir, original });
|
|
225
|
-
}
|
|
226
|
-
if (!dirs.length)
|
|
227
|
-
return;
|
|
228
|
-
const stats = [fs.statSync(dirname(dirs[0].resolved))];
|
|
229
|
-
for (const [i, dir] of dirs.entries()) {
|
|
230
|
-
stats.push(__create(dir.original, dir.resolved, stats[i]));
|
|
231
|
-
}
|
|
232
|
-
return dirs[0].original;
|
|
245
|
+
__create(original, target);
|
|
246
|
+
return firstCreated;
|
|
233
247
|
}
|
|
234
248
|
export function readdir(path, options = {}) {
|
|
235
249
|
path = normalizePath(path);
|
package/dist/vfs/vcache.js
CHANGED
|
@@ -139,7 +139,7 @@ export class VCache {
|
|
|
139
139
|
try {
|
|
140
140
|
if (!node.dirty)
|
|
141
141
|
continue;
|
|
142
|
-
const _ = __addDisposableResource(env_1, await node.lock('
|
|
142
|
+
const _ = __addDisposableResource(env_1, await node.lock('rw'), false);
|
|
143
143
|
await node.sync();
|
|
144
144
|
if (!node.refs)
|
|
145
145
|
this.evict(node);
|
|
@@ -160,7 +160,7 @@ export class VCache {
|
|
|
160
160
|
try {
|
|
161
161
|
if (!node.dirty)
|
|
162
162
|
continue;
|
|
163
|
-
const _ = __addDisposableResource(env_2, node.lockSync('
|
|
163
|
+
const _ = __addDisposableResource(env_2, node.lockSync('rw'), false);
|
|
164
164
|
node.syncSync();
|
|
165
165
|
if (!node.refs)
|
|
166
166
|
this.evict(node);
|
package/dist/vfs/vnode.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export declare class VNode extends RwLockable {
|
|
|
19
19
|
readonly data: Resource<number>;
|
|
20
20
|
/** Ranges of `data` that have been written but not yet synced to the backend */
|
|
21
21
|
protected dirtyRanges: Range[];
|
|
22
|
+
/** The smallest size this vnode has been truncated to since the last sync, if it has been truncated */
|
|
23
|
+
protected truncatedTo?: number;
|
|
22
24
|
/** Whether `inode` has changes that have not been synced to the backend */
|
|
23
25
|
metadataDirty: boolean;
|
|
24
26
|
/** The number of active references: open handles and in-progress VFS operations */
|
|
@@ -60,6 +62,10 @@ export declare class VNode extends RwLockable {
|
|
|
60
62
|
* Note an extending truncate creates a hole, which reads as zeroes.
|
|
61
63
|
*/
|
|
62
64
|
truncate(length: number): void;
|
|
65
|
+
/**
|
|
66
|
+
* Whether the backend has to be told about a truncation before the dirty data is written.
|
|
67
|
+
*/
|
|
68
|
+
protected get needsPreTruncate(): boolean;
|
|
63
69
|
/** Write all unsynced data to the backend, then metadata */
|
|
64
70
|
sync(): Promise<void>;
|
|
65
71
|
syncSync(): void;
|
package/dist/vfs/vnode.js
CHANGED
|
@@ -19,6 +19,8 @@ export class VNode extends RwLockable {
|
|
|
19
19
|
data;
|
|
20
20
|
/** Ranges of `data` that have been written but not yet synced to the backend */
|
|
21
21
|
dirtyRanges = [];
|
|
22
|
+
/** The smallest size this vnode has been truncated to since the last sync, if it has been truncated */
|
|
23
|
+
truncatedTo;
|
|
22
24
|
/** Whether `inode` has changes that have not been synced to the backend */
|
|
23
25
|
metadataDirty = false;
|
|
24
26
|
/** The number of active references: open handles and in-progress VFS operations */
|
|
@@ -151,6 +153,7 @@ export class VNode extends RwLockable {
|
|
|
151
153
|
* Note an extending truncate creates a hole, which reads as zeroes.
|
|
152
154
|
*/
|
|
153
155
|
truncate(length) {
|
|
156
|
+
this.truncatedTo = Math.min(this.truncatedTo ?? length, length);
|
|
154
157
|
this.data.size = length;
|
|
155
158
|
this.dirtyRanges = this.dirtyRanges
|
|
156
159
|
.filter(range => range.start < length)
|
|
@@ -160,8 +163,17 @@ export class VNode extends RwLockable {
|
|
|
160
163
|
this.inode.ctimeMs = Date.now();
|
|
161
164
|
this.metadataDirty = true;
|
|
162
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Whether the backend has to be told about a truncation before the dirty data is written.
|
|
168
|
+
*/
|
|
169
|
+
get needsPreTruncate() {
|
|
170
|
+
return !this.bypassCache && this.truncatedTo !== undefined && this.truncatedTo < this.inode.size && !this.fs.attributes.has('no_write');
|
|
171
|
+
}
|
|
163
172
|
/** Write all unsynced data to the backend, then metadata */
|
|
164
173
|
async sync() {
|
|
174
|
+
if (this.needsPreTruncate)
|
|
175
|
+
await this.fs.touch(this.path, { size: this.truncatedTo });
|
|
176
|
+
this.truncatedTo = undefined;
|
|
165
177
|
for (const range of this.dirtyRanges) {
|
|
166
178
|
for (let pos = range.start; pos < range.end;) {
|
|
167
179
|
const region = this.data.regionAt(pos);
|
|
@@ -180,6 +192,9 @@ export class VNode extends RwLockable {
|
|
|
180
192
|
this.metadataDirty = false;
|
|
181
193
|
}
|
|
182
194
|
syncSync() {
|
|
195
|
+
if (this.needsPreTruncate)
|
|
196
|
+
this.fs.touchSync(this.path, { size: this.truncatedTo });
|
|
197
|
+
this.truncatedTo = undefined;
|
|
183
198
|
for (const range of this.dirtyRanges) {
|
|
184
199
|
for (let pos = range.start; pos < range.end;) {
|
|
185
200
|
const region = this.data.regionAt(pos);
|
package/package.json
CHANGED
package/scripts/make-index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { matchesGlob, relative,
|
|
2
|
+
import { lstatSync, readdirSync, writeFileSync, constants as c } from 'node:fs';
|
|
3
|
+
import { join, matchesGlob, relative, resolve } from 'node:path/posix';
|
|
4
4
|
import { parseArgs, styleText } from 'node:util';
|
|
5
5
|
|
|
6
6
|
const { values: options, positionals } = parseArgs({
|
|
@@ -47,6 +47,34 @@ const resolvedRoot = root || '.';
|
|
|
47
47
|
|
|
48
48
|
const entries = new Map();
|
|
49
49
|
|
|
50
|
+
/** @type {Record<string, import('node:util').InspectColor>} */
|
|
51
|
+
const typeMap = {
|
|
52
|
+
file: 'green',
|
|
53
|
+
' dir': 'blue',
|
|
54
|
+
' sym': 'cyan',
|
|
55
|
+
' dev': 'yellow',
|
|
56
|
+
sock: 'magenta',
|
|
57
|
+
fifo: 'blue',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function getTypeText(/** @type {{ mode: number }} */ stats) {
|
|
61
|
+
switch (stats.mode & c.S_IFMT) {
|
|
62
|
+
case c.S_IFBLK:
|
|
63
|
+
case c.S_IFCHR:
|
|
64
|
+
return ' dev';
|
|
65
|
+
case c.S_IFIFO:
|
|
66
|
+
return 'fifo';
|
|
67
|
+
case c.S_IFSOCK:
|
|
68
|
+
return 'sock';
|
|
69
|
+
case c.S_IFREG:
|
|
70
|
+
return 'file';
|
|
71
|
+
case c.S_IFDIR:
|
|
72
|
+
return ' dir';
|
|
73
|
+
default:
|
|
74
|
+
throw `Unknown file type: 0o${(stats.mode & c.S_IFMT).toString(8)}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
50
78
|
/**
|
|
51
79
|
* @param {string} path
|
|
52
80
|
*/
|
|
@@ -57,22 +85,15 @@ function computeEntries(path) {
|
|
|
57
85
|
return;
|
|
58
86
|
}
|
|
59
87
|
|
|
60
|
-
const stats =
|
|
88
|
+
const stats = lstatSync(path);
|
|
61
89
|
|
|
62
|
-
|
|
63
|
-
entries.set('/' + relative(resolvedRoot, path), stats);
|
|
64
|
-
if (options.verbose) {
|
|
65
|
-
console.log(`${styleText('green', 'file')} ${path}`);
|
|
66
|
-
}
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
90
|
+
const type = getTypeText(stats);
|
|
69
91
|
|
|
70
|
-
for (const file of readdirSync(path)) {
|
|
71
|
-
computeEntries(join(path, file));
|
|
72
|
-
}
|
|
73
92
|
entries.set('/' + relative(resolvedRoot, path), stats);
|
|
74
|
-
if (options.verbose) {
|
|
75
|
-
|
|
93
|
+
if (options.verbose) console.log(`${styleText(typeMap[type] || 'white', type)} ${path}`);
|
|
94
|
+
|
|
95
|
+
if (stats.isDirectory()) {
|
|
96
|
+
for (const file of readdirSync(path)) computeEntries(join(path, file));
|
|
76
97
|
}
|
|
77
98
|
} catch (/** @type {any} */ e) {
|
|
78
99
|
if (!options.quiet) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
-
import { Fetch, configureSingle, fs, mounts, type FetchFS } from '@zenfs/core';
|
|
2
|
+
import { CopyOnWrite, Fetch, InMemory, configureSingle, fs, mounts, resolveMountConfig, type FetchFS } from '@zenfs/core';
|
|
3
3
|
import assert from 'node:assert/strict';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { after, suite, test } from 'node:test';
|
|
@@ -47,6 +47,29 @@ suite('Fetch with `disableAsyncCache`', () => {
|
|
|
47
47
|
});
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
suite('CopyOnWrite over uncached Fetch #301', () => {
|
|
51
|
+
test('Overwriting a base-layer file does not need a synchronous copy-up read', async () => {
|
|
52
|
+
const readable = await resolveMountConfig({ backend: Fetch, disableAsyncCache: true, baseUrl, index: baseUrl + indexPath });
|
|
53
|
+
|
|
54
|
+
fs.mount('/cow', await resolveMountConfig({ backend: CopyOnWrite, readable, writable: InMemory.create({}) }));
|
|
55
|
+
|
|
56
|
+
// `w+` truncates first, so none of the base file survives and nothing has to be read from it
|
|
57
|
+
fs.writeFileSync('/cow/49chars.txt', 'replaced');
|
|
58
|
+
|
|
59
|
+
assert.equal(fs.readFileSync('/cow/49chars.txt', 'utf8'), 'replaced');
|
|
60
|
+
assert.equal(fs.statSync('/cow/49chars.txt').size, 8);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('Partially overwriting an uncached base-layer file still throws', () => {
|
|
64
|
+
const fd = fs.openSync('/cow/a.js', 'r+');
|
|
65
|
+
fs.writeSync(fd, 'x', 0);
|
|
66
|
+
|
|
67
|
+
// The bytes the write leaves alone have to be copied up, which `Fetch` can not do synchronously.
|
|
68
|
+
// Writes go through the vnode cache, so this surfaces when the handle is flushed rather than at `write`.
|
|
69
|
+
assert.throws(() => fs.fsyncSync(fd), { code: 'EAGAIN' });
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
50
73
|
after(async () => {
|
|
51
74
|
await (mounts.get('/') as FetchFS)._asyncDone;
|
|
52
75
|
await server.terminate();
|
|
@@ -78,4 +78,12 @@ suite('Context', () => {
|
|
|
78
78
|
|
|
79
79
|
assert.equal(ctx.realpathSync('.'), '/test');
|
|
80
80
|
});
|
|
81
|
+
|
|
82
|
+
test('copyFileSync should keep the context #307', () => {
|
|
83
|
+
ctx.writeFileSync('/source.txt', 'not in real root!');
|
|
84
|
+
ctx.copyFileSync('/source.txt', '/copy.txt');
|
|
85
|
+
|
|
86
|
+
assert.equal(ctx.readFileSync('/copy.txt', 'utf-8'), 'not in real root!');
|
|
87
|
+
assert(fs.readdirSync('/ctx').includes('copy.txt'));
|
|
88
|
+
});
|
|
81
89
|
});
|
package/tests/common.ts
CHANGED
|
@@ -21,8 +21,6 @@ const setup = await import(setupPath).catch(error => {
|
|
|
21
21
|
// Satisfies is used to make sure that ZenFS is fully type compatible with Node.js
|
|
22
22
|
export const fs = (setup.fs || defaultFS) as typeof defaultFS satisfies NodeFS;
|
|
23
23
|
|
|
24
|
-
export const Stats = (setup.Stats || defaultFS.Stats) as typeof defaultFS.Stats;
|
|
25
|
-
|
|
26
24
|
/**
|
|
27
25
|
* A feature or function a backend may not support.
|
|
28
26
|
* Tests that need one are skipped when the backend under test does not have it.
|
|
@@ -49,4 +49,65 @@ suite('Concurrency', config('write', 'async'), () => {
|
|
|
49
49
|
await a.close();
|
|
50
50
|
await b.close();
|
|
51
51
|
});
|
|
52
|
+
|
|
53
|
+
test('reads concurrent with an overwrite of the same file do not fail #303', async () => {
|
|
54
|
+
await fs.promises.writeFile('/issue-303.txt', 'original contents');
|
|
55
|
+
|
|
56
|
+
/* Every handle for a file shares one vnode, so closing any of these flushes whatever is dirty.
|
|
57
|
+
Syncing took a shared lock, letting several of these run at once and corrupt each other. */
|
|
58
|
+
for (let round = 0; round < 20; round++) {
|
|
59
|
+
const contents = `rewritten contents for round ${round}`;
|
|
60
|
+
|
|
61
|
+
const results = await Promise.allSettled([
|
|
62
|
+
...Array.from({ length: 4 }, () => fs.promises.readFile('/issue-303.txt', 'utf8')),
|
|
63
|
+
fs.promises.writeFile('/issue-303.txt', contents),
|
|
64
|
+
...Array.from({ length: 4 }, () => fs.promises.readFile('/issue-303.txt', 'utf8')),
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
const rejected = results.filter(r => r.status === 'rejected');
|
|
68
|
+
assert.deepEqual(
|
|
69
|
+
rejected.map(r => r.reason.code ?? r.reason.message),
|
|
70
|
+
[]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
assert.equal(await fs.promises.readFile('/issue-303.txt', 'utf8'), contents);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('closing a handle does not fail because another handle has unsynced writes #303', async () => {
|
|
78
|
+
await fs.promises.writeFile('/issue-303-close.txt', 'original');
|
|
79
|
+
|
|
80
|
+
const reader = await fs.promises.open('/issue-303-close.txt', 'r');
|
|
81
|
+
const writer = await fs.promises.open('/issue-303-close.txt', 'r+');
|
|
82
|
+
|
|
83
|
+
// The reader wrote nothing, so its close must not report the writer's dirty data as EBUSY
|
|
84
|
+
const [closed] = await Promise.allSettled([reader.close(), writer.write(new TextEncoder().encode('DIRTY'), 0, 5, 0)]);
|
|
85
|
+
assert.equal(closed.status, 'fulfilled');
|
|
86
|
+
|
|
87
|
+
await writer.close();
|
|
88
|
+
assert.equal(await fs.promises.readFile('/issue-303-close.txt', 'utf8'), 'DIRTYnal');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('concurrent recursive mkdir of overlapping trees does not fail #308', async () => {
|
|
92
|
+
const paths = ['/308/a', '/308/a', '/308/a/b', '/308/a/b', '/308/c', '/308/c', '/308-2', '/308-2'];
|
|
93
|
+
|
|
94
|
+
const results = await Promise.allSettled(paths.map(path => fs.promises.mkdir(path, { recursive: true })));
|
|
95
|
+
|
|
96
|
+
const rejected = results.filter(r => r.status == 'rejected');
|
|
97
|
+
assert.deepEqual(
|
|
98
|
+
rejected.map(r => r.reason.code ?? r.reason.message),
|
|
99
|
+
[]
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
for (const path of paths) assert((await fs.promises.stat(path)).isDirectory());
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('parallel recursive mkdir of the same path creates it once #308', async () => {
|
|
106
|
+
const results = await Promise.all(Array.from({ length: 8 }, () => fs.promises.mkdir('/308-same/nested', { recursive: true })));
|
|
107
|
+
|
|
108
|
+
assert.deepEqual(
|
|
109
|
+
results.filter(result => result !== undefined),
|
|
110
|
+
['/308-same']
|
|
111
|
+
);
|
|
112
|
+
});
|
|
52
113
|
});
|
|
@@ -45,6 +45,13 @@ suite('Directories', config('directories'), () => {
|
|
|
45
45
|
assert.equal((await fs.promises.stat('/recursiveP/A/B/C/D')).mode, fs.constants.S_IFDIR | 0o777);
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
test('mkdir, recursive with a file in the way', config('async'), async () => {
|
|
49
|
+
await fs.promises.writeFile('/recursiveF', 'Sample content');
|
|
50
|
+
|
|
51
|
+
await assert.rejects(fs.promises.mkdir('/recursiveF', { recursive: true }), { code: 'EEXIST', path: '/recursiveF' });
|
|
52
|
+
await assert.rejects(fs.promises.mkdir('/recursiveF/A/B', { recursive: true }), { code: 'ENOTDIR', path: '/recursiveF/A/B' });
|
|
53
|
+
});
|
|
54
|
+
|
|
48
55
|
test('mkdirSync, recursive', config('sync'), () => {
|
|
49
56
|
assert.equal(fs.mkdirSync('/recursiveS/A/B', { recursive: true, mode: 0o755 }), '/recursiveS');
|
|
50
57
|
assert.equal(fs.mkdirSync('/recursiveS/A/B/C/D', { recursive: true, mode: 0o777 }), '/recursiveS/A/B/C');
|
|
@@ -57,6 +64,13 @@ suite('Directories', config('directories'), () => {
|
|
|
57
64
|
assert.equal(fs.statSync('/recursiveS/A/B/C/D').mode, fs.constants.S_IFDIR | 0o777);
|
|
58
65
|
});
|
|
59
66
|
|
|
67
|
+
test('mkdirSync, recursive with a file in the way', config('sync'), () => {
|
|
68
|
+
fs.writeFileSync('/recursiveSF', 'Sample content');
|
|
69
|
+
|
|
70
|
+
assert.throws(() => fs.mkdirSync('/recursiveSF', { recursive: true }), { code: 'EEXIST', path: '/recursiveSF' });
|
|
71
|
+
assert.throws(() => fs.mkdirSync('/recursiveSF/A/B', { recursive: true }), { code: 'ENOTDIR', path: '/recursiveSF/A/B' });
|
|
72
|
+
});
|
|
73
|
+
|
|
60
74
|
test('rmdir (non-empty)', config('async'), async () => {
|
|
61
75
|
await fs.promises.mkdir('/rmdirTest');
|
|
62
76
|
await fs.promises.mkdir('/rmdirTest/rmdirTest2');
|
|
@@ -165,4 +179,11 @@ suite('Directories', config('directories'), () => {
|
|
|
165
179
|
fs.writeFileSync('/мой-файл.txt', 'HELLO!', 'utf-8');
|
|
166
180
|
assert(fs.readdirSync('/').includes('мой-файл.txt'));
|
|
167
181
|
});
|
|
182
|
+
|
|
183
|
+
test('re-creating a deleted path still shows up in readdir #305', config('sync'), () => {
|
|
184
|
+
fs.unlinkSync('/elipses.txt');
|
|
185
|
+
fs.writeFileSync('/elipses.txt', '...');
|
|
186
|
+
|
|
187
|
+
assert(fs.readdirSync('/').includes('elipses.txt'));
|
|
188
|
+
});
|
|
168
189
|
});
|
package/tests/fs/links.test.ts
CHANGED
|
@@ -83,4 +83,150 @@ suite('Links', config('symlinks'), () => {
|
|
|
83
83
|
const linkContent = await fs.promises.readFile(link, 'utf8');
|
|
84
84
|
assert.equal(targetContent, linkContent);
|
|
85
85
|
});
|
|
86
|
+
|
|
87
|
+
test('mkdir does not follow a symlink at the final component', config('async'), async () => {
|
|
88
|
+
await fs.promises.mkdir('/mkdir-real');
|
|
89
|
+
await fs.promises.symlink('/mkdir-real', '/mkdir-link');
|
|
90
|
+
await fs.promises.writeFile('/mkdir-file', 'contents');
|
|
91
|
+
await fs.promises.symlink('/mkdir-file', '/mkdir-flink');
|
|
92
|
+
await fs.promises.symlink('/mkdir-missing', '/mkdir-dangling');
|
|
93
|
+
|
|
94
|
+
await assert.rejects(fs.promises.mkdir('/mkdir-link'), { code: 'EEXIST' });
|
|
95
|
+
await assert.rejects(fs.promises.mkdir('/mkdir-dangling'), { code: 'EEXIST' });
|
|
96
|
+
await assert.rejects(fs.promises.mkdir('/mkdir-flink', { recursive: true }), { code: 'EEXIST' });
|
|
97
|
+
await assert.rejects(fs.promises.mkdir('/mkdir-dangling', { recursive: true }), { code: 'ENOENT' });
|
|
98
|
+
|
|
99
|
+
// A link pointing at a directory satisfies a recursive mkdir and is left as a link
|
|
100
|
+
assert.equal(await fs.promises.mkdir('/mkdir-link', { recursive: true }), undefined);
|
|
101
|
+
assert((await fs.promises.lstat('/mkdir-link')).isSymbolicLink());
|
|
102
|
+
assert(!(await fs.promises.exists('/mkdir-missing')));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('mkdir follows symlinks for intermediate components', config('async'), async () => {
|
|
106
|
+
assert.equal(await fs.promises.mkdir('/mkdir-link/direct'), undefined);
|
|
107
|
+
assert.equal(await fs.promises.mkdir('/mkdir-link/nested/deep', { recursive: true }), '/mkdir-link/nested');
|
|
108
|
+
|
|
109
|
+
assert.deepEqual((await fs.promises.readdir('/mkdir-real')).sort(), ['direct', 'nested']);
|
|
110
|
+
assert((await fs.promises.stat('/mkdir-real/nested/deep')).isDirectory());
|
|
111
|
+
|
|
112
|
+
await assert.rejects(fs.promises.mkdir('/mkdir-flink/x', { recursive: true }), { code: 'ENOTDIR' });
|
|
113
|
+
|
|
114
|
+
// Node reports the link itself here, unlike `mkdirSync` which reports the requested path with ENOENT
|
|
115
|
+
await assert.rejects(fs.promises.mkdir('/mkdir-dangling/x', { recursive: true }), { code: 'ENOTDIR', path: '/mkdir-dangling' });
|
|
116
|
+
assert(!(await fs.promises.exists('/mkdir-missing')));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('mkdirSync does not follow a symlink at the final component', config('sync'), () => {
|
|
120
|
+
fs.mkdirSync('/mkdirS-real');
|
|
121
|
+
fs.symlinkSync('/mkdirS-real', '/mkdirS-link');
|
|
122
|
+
fs.writeFileSync('/mkdirS-file', 'contents');
|
|
123
|
+
fs.symlinkSync('/mkdirS-file', '/mkdirS-flink');
|
|
124
|
+
fs.symlinkSync('/mkdirS-missing', '/mkdirS-dangling');
|
|
125
|
+
|
|
126
|
+
assert.throws(() => fs.mkdirSync('/mkdirS-link'), { code: 'EEXIST' });
|
|
127
|
+
assert.throws(() => fs.mkdirSync('/mkdirS-dangling'), { code: 'EEXIST' });
|
|
128
|
+
assert.throws(() => fs.mkdirSync('/mkdirS-flink', { recursive: true }), { code: 'EEXIST' });
|
|
129
|
+
assert.throws(() => fs.mkdirSync('/mkdirS-dangling', { recursive: true }), { code: 'ENOENT' });
|
|
130
|
+
|
|
131
|
+
assert.equal(fs.mkdirSync('/mkdirS-link', { recursive: true }), undefined);
|
|
132
|
+
assert(fs.lstatSync('/mkdirS-link').isSymbolicLink());
|
|
133
|
+
assert(!fs.existsSync('/mkdirS-missing'));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('mkdirSync follows symlinks for intermediate components', config('sync'), () => {
|
|
137
|
+
assert.equal(fs.mkdirSync('/mkdirS-link/direct'), undefined);
|
|
138
|
+
assert.equal(fs.mkdirSync('/mkdirS-link/nested/deep', { recursive: true }), '/mkdirS-link/nested');
|
|
139
|
+
|
|
140
|
+
assert.deepEqual(fs.readdirSync('/mkdirS-real').sort(), ['direct', 'nested']);
|
|
141
|
+
assert(fs.statSync('/mkdirS-real/nested/deep').isDirectory());
|
|
142
|
+
|
|
143
|
+
assert.throws(() => fs.mkdirSync('/mkdirS-flink/x', { recursive: true }), { code: 'ENOTDIR' });
|
|
144
|
+
assert.throws(() => fs.mkdirSync('/mkdirS-dangling/x', { recursive: true }), { code: 'ENOENT', path: '/mkdirS-dangling/x' });
|
|
145
|
+
assert(!fs.existsSync('/mkdirS-missing'));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('writeFile and appendFile follow symlinks', config('async'), async () => {
|
|
149
|
+
await fs.promises.mkdir('/wf-real');
|
|
150
|
+
await fs.promises.symlink('/wf-real', '/wf-link');
|
|
151
|
+
await fs.promises.writeFile('/wf-real/f.txt', 'original');
|
|
152
|
+
await fs.promises.symlink('/wf-real/f.txt', '/wf-flink');
|
|
153
|
+
await fs.promises.symlink('/wf-missing.txt', '/wf-dangling');
|
|
154
|
+
|
|
155
|
+
await fs.promises.writeFile('/wf-link/new.txt', 'hi');
|
|
156
|
+
assert.equal(await fs.promises.readFile('/wf-real/new.txt', 'utf8'), 'hi');
|
|
157
|
+
|
|
158
|
+
// A link to a file is written through, not replaced
|
|
159
|
+
await fs.promises.writeFile('/wf-flink', 'replaced');
|
|
160
|
+
await fs.promises.appendFile('/wf-flink', '+');
|
|
161
|
+
assert.equal(await fs.promises.readFile('/wf-real/f.txt', 'utf8'), 'replaced+');
|
|
162
|
+
assert((await fs.promises.lstat('/wf-flink')).isSymbolicLink());
|
|
163
|
+
|
|
164
|
+
// Writing through a dangling link creates its target
|
|
165
|
+
await fs.promises.writeFile('/wf-dangling', 'made');
|
|
166
|
+
assert.equal(await fs.promises.readFile('/wf-missing.txt', 'utf8'), 'made');
|
|
167
|
+
assert((await fs.promises.lstat('/wf-dangling')).isSymbolicLink());
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('writeFileSync and appendFileSync follow symlinks', config('sync'), () => {
|
|
171
|
+
fs.mkdirSync('/wfS-real');
|
|
172
|
+
fs.symlinkSync('/wfS-real', '/wfS-link');
|
|
173
|
+
fs.writeFileSync('/wfS-real/f.txt', 'original');
|
|
174
|
+
fs.symlinkSync('/wfS-real/f.txt', '/wfS-flink');
|
|
175
|
+
fs.symlinkSync('/wfS-missing.txt', '/wfS-dangling');
|
|
176
|
+
|
|
177
|
+
fs.writeFileSync('/wfS-link/new.txt', 'hi');
|
|
178
|
+
assert.equal(fs.readFileSync('/wfS-real/new.txt', 'utf8'), 'hi');
|
|
179
|
+
|
|
180
|
+
fs.writeFileSync('/wfS-flink', 'replaced');
|
|
181
|
+
fs.appendFileSync('/wfS-flink', '+');
|
|
182
|
+
assert.equal(fs.readFileSync('/wfS-real/f.txt', 'utf8'), 'replaced+');
|
|
183
|
+
assert(fs.lstatSync('/wfS-flink').isSymbolicLink());
|
|
184
|
+
|
|
185
|
+
fs.writeFileSync('/wfS-dangling', 'made');
|
|
186
|
+
assert.equal(fs.readFileSync('/wfS-missing.txt', 'utf8'), 'made');
|
|
187
|
+
assert(fs.lstatSync('/wfS-dangling').isSymbolicLink());
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('cp preserves a symlink by default #304', async () => {
|
|
191
|
+
await fs.promises.mkdir('/cp-src');
|
|
192
|
+
await fs.promises.writeFile('/cp-src/real.txt', 'contents');
|
|
193
|
+
await fs.promises.symlink('real.txt', '/cp-src/link.txt');
|
|
194
|
+
|
|
195
|
+
await fs.promises.cp('/cp-src', '/cp-dst', { recursive: true });
|
|
196
|
+
|
|
197
|
+
const stats = await fs.promises.lstat('/cp-dst/link.txt');
|
|
198
|
+
assert(stats.isSymbolicLink());
|
|
199
|
+
|
|
200
|
+
// A relative target is resolved against the link's directory, so the copy points at the original file
|
|
201
|
+
assert.equal(await fs.promises.readlink('/cp-dst/link.txt'), '/cp-src/real.txt');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('cp with verbatimSymlinks keeps the target unresolved', async () => {
|
|
205
|
+
await fs.promises.cp('/cp-src', '/cp-verbatim', { recursive: true, verbatimSymlinks: true });
|
|
206
|
+
|
|
207
|
+
assert((await fs.promises.lstat('/cp-verbatim/link.txt')).isSymbolicLink());
|
|
208
|
+
assert.equal(await fs.promises.readlink('/cp-verbatim/link.txt'), 'real.txt');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('cp with dereference copies the symlink target contents #304', async () => {
|
|
212
|
+
await fs.promises.cp('/cp-src', '/cp-deref', { recursive: true, dereference: true });
|
|
213
|
+
|
|
214
|
+
const stats = await fs.promises.lstat('/cp-deref/link.txt');
|
|
215
|
+
assert(!stats.isSymbolicLink());
|
|
216
|
+
assert.equal(await fs.promises.readFile('/cp-deref/link.txt', 'utf8'), 'contents');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('cpSync preserves a symlink #304', () => {
|
|
220
|
+
fs.writeFileSync('/cp-sync-target.txt', 'contents');
|
|
221
|
+
fs.symlinkSync('cp-sync-target.txt', '/cp-sync-link');
|
|
222
|
+
|
|
223
|
+
fs.cpSync('/cp-sync-link', '/cp-sync-link-copy');
|
|
224
|
+
|
|
225
|
+
const stats = fs.lstatSync('/cp-sync-link-copy');
|
|
226
|
+
assert(stats.isSymbolicLink());
|
|
227
|
+
assert.equal(fs.readlinkSync('/cp-sync-link-copy'), '/cp-sync-target.txt');
|
|
228
|
+
|
|
229
|
+
fs.cpSync('/cp-sync-link', '/cp-sync-link-verbatim', { verbatimSymlinks: true });
|
|
230
|
+
assert.equal(fs.readlinkSync('/cp-sync-link-verbatim'), 'cp-sync-target.txt');
|
|
231
|
+
});
|
|
86
232
|
});
|
package/tests/native/setup.ts
CHANGED