@zenfs/core 1.7.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backends/backend.js +3 -4
- package/dist/backends/fetch.d.ts +17 -18
- package/dist/backends/fetch.js +95 -58
- package/dist/backends/index.d.ts +2 -1
- package/dist/backends/index.js +2 -1
- package/dist/backends/memory.d.ts +1 -1
- package/dist/backends/overlay.d.ts +7 -2
- package/dist/backends/overlay.js +32 -9
- package/dist/backends/passthrough.d.ts +4 -0
- package/dist/backends/passthrough.js +128 -0
- package/dist/backends/port/fs.d.ts +9 -44
- package/dist/backends/port/fs.js +93 -116
- package/dist/backends/port/rpc.d.ts +8 -5
- package/dist/backends/port/rpc.js +9 -7
- package/dist/backends/store/file_index.d.ts +38 -0
- package/dist/backends/store/file_index.js +76 -0
- package/dist/backends/store/fs.d.ts +55 -34
- package/dist/backends/store/fs.js +417 -233
- package/dist/backends/store/index_fs.d.ts +34 -0
- package/dist/backends/store/index_fs.js +67 -0
- package/dist/backends/store/inode.d.ts +26 -8
- package/dist/backends/store/inode.js +92 -91
- package/dist/backends/store/simple.d.ts +20 -20
- package/dist/backends/store/simple.js +3 -4
- package/dist/backends/store/store.d.ts +12 -12
- package/dist/backends/store/store.js +4 -6
- package/dist/devices.d.ts +11 -10
- package/dist/devices.js +15 -11
- package/dist/file.d.ts +111 -7
- package/dist/file.js +319 -71
- package/dist/filesystem.d.ts +22 -4
- package/dist/mixins/mutexed.d.ts +7 -2
- package/dist/mixins/mutexed.js +56 -0
- package/dist/mixins/sync.d.ts +1 -1
- package/dist/stats.d.ts +12 -6
- package/dist/stats.js +14 -6
- package/dist/utils.d.ts +17 -3
- package/dist/utils.js +32 -10
- package/dist/vfs/constants.d.ts +2 -2
- package/dist/vfs/constants.js +2 -2
- package/dist/vfs/dir.js +3 -1
- package/dist/vfs/index.js +4 -1
- package/dist/vfs/promises.js +31 -11
- package/dist/vfs/shared.js +2 -0
- package/dist/vfs/sync.js +25 -13
- package/dist/vfs/types.d.ts +15 -0
- package/package.json +2 -3
- package/readme.md +2 -2
- package/scripts/test.js +73 -11
- package/tests/common/mutex.test.ts +1 -1
- package/tests/fetch/run.sh +16 -0
- package/tests/fetch/server.ts +49 -0
- package/tests/fetch/setup.ts +13 -0
- package/tests/fs/read.test.ts +10 -10
- package/tests/fs/times.test.ts +2 -2
- package/tests/setup/index.ts +38 -0
- package/tests/setup/port.ts +15 -0
- package/dist/backends/file_index.d.ts +0 -63
- package/dist/backends/file_index.js +0 -163
- package/tests/common/async.test.ts +0 -31
- package/tests/setup/cow+fetch.ts +0 -45
- /package/tests/fs/{appendFile.test.ts → append.test.ts} +0 -0
package/scripts/test.js
CHANGED
|
@@ -16,7 +16,10 @@ const { values: options, positionals } = parseArgs({
|
|
|
16
16
|
build: { short: 'b', type: 'boolean', default: false },
|
|
17
17
|
common: { short: 'c', type: 'boolean', default: false },
|
|
18
18
|
coverage: { type: 'string', default: 'tests/.coverage' },
|
|
19
|
+
preserve: { short: 'p', type: 'boolean' },
|
|
19
20
|
'exit-on-fail': { short: 'e', type: 'boolean' },
|
|
21
|
+
report: { type: 'boolean' },
|
|
22
|
+
clean: { type: 'boolean' },
|
|
20
23
|
},
|
|
21
24
|
allowPositionals: true,
|
|
22
25
|
});
|
|
@@ -36,7 +39,12 @@ Options:
|
|
|
36
39
|
-q, --quiet Don't output normal messages
|
|
37
40
|
-t, --test <glob> Which FS test suite(s) to run
|
|
38
41
|
-f, --force Whether to use --test-force-exit
|
|
39
|
-
|
|
42
|
+
|
|
43
|
+
Coverage:
|
|
44
|
+
--coverage <dir> Override the default coverage data directory
|
|
45
|
+
-p,--preserve Do not delete or report coverage data
|
|
46
|
+
--report ONLY report coverage
|
|
47
|
+
--clean ONLY clean up coverage directory`);
|
|
40
48
|
process.exit();
|
|
41
49
|
}
|
|
42
50
|
|
|
@@ -45,6 +53,19 @@ if (options.quiet && options.verbose) {
|
|
|
45
53
|
process.exit(1);
|
|
46
54
|
}
|
|
47
55
|
|
|
56
|
+
process.env.NODE_V8_COVERAGE = options.coverage;
|
|
57
|
+
|
|
58
|
+
if (options.clean) {
|
|
59
|
+
rmSync(options.coverage, { recursive: true, force: true });
|
|
60
|
+
process.exit();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (options.report) {
|
|
64
|
+
execSync('npx c8 report --reporter=text', { stdio: 'inherit' });
|
|
65
|
+
rmSync(options.coverage, { recursive: true, force: true });
|
|
66
|
+
process.exit();
|
|
67
|
+
}
|
|
68
|
+
|
|
48
69
|
options.verbose && options.force && console.debug('Forcing tests to exit (--test-force-exit)');
|
|
49
70
|
|
|
50
71
|
if (options.build) {
|
|
@@ -73,19 +94,56 @@ if (options.auto) {
|
|
|
73
94
|
!options.quiet && console.log(`Auto-detected ${sum} test setup files`);
|
|
74
95
|
}
|
|
75
96
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Colorizes some text
|
|
99
|
+
* @param {string} text Text to color
|
|
100
|
+
* @param {string | number} code ANSI escape code
|
|
101
|
+
* @returns
|
|
102
|
+
*/
|
|
103
|
+
function color(text, code) {
|
|
104
|
+
return `\x1b[${code}m${text}\x1b[0m`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function status(name) {
|
|
108
|
+
const start = performance.now();
|
|
109
|
+
|
|
110
|
+
const time = () => {
|
|
111
|
+
let delta = Math.round(performance.now() - start),
|
|
112
|
+
unit = 'ms';
|
|
113
|
+
|
|
114
|
+
if (delta > 5000) {
|
|
115
|
+
delta /= 1000;
|
|
116
|
+
unit = 's';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return color(`(${delta} ${unit})`, '2;37');
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
pass() {
|
|
124
|
+
if (!options.quiet) console.log(`${color('passed', 32)}: ${name} ${time()}`);
|
|
125
|
+
},
|
|
126
|
+
fail() {
|
|
127
|
+
console.error(`${color('failed', '1;31')}: ${name} ${time()}`);
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
if (options['exit-on-fail']) process.exit();
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!options.preserve) rmSync(options.coverage, { force: true, recursive: true });
|
|
135
|
+
mkdirSync(options.coverage, { recursive: true });
|
|
79
136
|
|
|
80
137
|
if (options.common) {
|
|
81
138
|
!options.quiet && console.log('Running common tests...');
|
|
139
|
+
const { pass, fail } = status('Common tests');
|
|
82
140
|
try {
|
|
83
141
|
execSync("tsx --test --experimental-test-coverage 'tests/*.test.ts' 'tests/**/!(fs)/*.test.ts'", {
|
|
84
142
|
stdio: ['ignore', options.verbose ? 'inherit' : 'ignore', 'inherit'],
|
|
85
143
|
});
|
|
144
|
+
pass();
|
|
86
145
|
} catch {
|
|
87
|
-
|
|
88
|
-
if (options['exit-on-fail']) process.exit(1);
|
|
146
|
+
fail();
|
|
89
147
|
}
|
|
90
148
|
}
|
|
91
149
|
|
|
@@ -97,18 +155,22 @@ for (const setupFile of positionals) {
|
|
|
97
155
|
continue;
|
|
98
156
|
}
|
|
99
157
|
|
|
100
|
-
!options.quiet && console.
|
|
158
|
+
!options.quiet && console.log('Running tests:', setupFile);
|
|
101
159
|
process.env.SETUP = setupFile;
|
|
102
160
|
|
|
161
|
+
const { pass, fail } = status(setupFile);
|
|
162
|
+
|
|
103
163
|
try {
|
|
104
164
|
execSync(['tsx --test --experimental-test-coverage', options.force ? '--test-force-exit' : '', testsGlob, process.env.CMD].join(' '), {
|
|
105
165
|
stdio: ['ignore', options.verbose ? 'inherit' : 'ignore', 'inherit'],
|
|
106
166
|
});
|
|
167
|
+
pass();
|
|
107
168
|
} catch {
|
|
108
|
-
|
|
109
|
-
if (options['exit-on-fail']) process.exit(1);
|
|
169
|
+
fail();
|
|
110
170
|
}
|
|
111
171
|
}
|
|
112
172
|
|
|
113
|
-
|
|
114
|
-
|
|
173
|
+
if (!options.preserve) {
|
|
174
|
+
execSync('npx c8 report --reporter=text', { stdio: 'inherit' });
|
|
175
|
+
rmSync(options.coverage, { recursive: true });
|
|
176
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/bash
|
|
2
|
+
|
|
3
|
+
# Credit: Dave Dopson, https://stackoverflow.com/a/246128/17637456
|
|
4
|
+
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
|
5
|
+
|
|
6
|
+
npx tsx $SCRIPT_DIR/server.ts &
|
|
7
|
+
PID=$!
|
|
8
|
+
|
|
9
|
+
echo "Waiting for server to start..."
|
|
10
|
+
until nc -z localhost 26514; do
|
|
11
|
+
sleep 0.5
|
|
12
|
+
done
|
|
13
|
+
|
|
14
|
+
npx zenfs-test $SCRIPT_DIR/setup.ts --preserve --force $@
|
|
15
|
+
|
|
16
|
+
kill $PID
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { data, tmp } from '../setup.js';
|
|
6
|
+
|
|
7
|
+
// If you change the port please update the setup file as well
|
|
8
|
+
const port = 26514;
|
|
9
|
+
|
|
10
|
+
const statusCodes = {
|
|
11
|
+
ENOENT: 404,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
execSync(`npm exec make-index -- ${data} --output ${tmp}/index.json --quiet`, { stdio: 'inherit' });
|
|
16
|
+
} catch (e: any) {
|
|
17
|
+
if (e.signal == 'SIGINT') {
|
|
18
|
+
console.log('Aborted whilst creating index.');
|
|
19
|
+
process.exit(0);
|
|
20
|
+
} else {
|
|
21
|
+
console.error('Index creation failed: ' + e.message);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const server = createServer((request, response) => {
|
|
27
|
+
const { url = '' } = request;
|
|
28
|
+
|
|
29
|
+
if (url == '/.ping') {
|
|
30
|
+
response.statusCode = 200;
|
|
31
|
+
response.end('pong');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const path = url == '/.index.json' ? join(tmp, 'index.json') : join(data, url.slice(1) || '');
|
|
36
|
+
try {
|
|
37
|
+
response.statusCode = 200;
|
|
38
|
+
response.end(readFileSync(path));
|
|
39
|
+
} catch (e: any) {
|
|
40
|
+
response.statusCode = statusCodes[e.code as keyof typeof statusCodes] || 400;
|
|
41
|
+
response.end();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
server.listen(port);
|
|
46
|
+
|
|
47
|
+
process.on('beforeExit', () => {
|
|
48
|
+
server.close().unref();
|
|
49
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { configureSingle, Fetch, InMemory, Overlay, resolveMountConfig } from '../../dist/index.js';
|
|
2
|
+
|
|
3
|
+
const baseUrl = 'http://localhost:26514';
|
|
4
|
+
|
|
5
|
+
await configureSingle({
|
|
6
|
+
backend: Overlay,
|
|
7
|
+
readable: await resolveMountConfig({
|
|
8
|
+
backend: Fetch,
|
|
9
|
+
baseUrl,
|
|
10
|
+
index: baseUrl + '/.index.json',
|
|
11
|
+
}),
|
|
12
|
+
writable: InMemory.create({ name: 'cow' }),
|
|
13
|
+
});
|
package/tests/fs/read.test.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { suite, test } from 'node:test';
|
|
3
3
|
import { fs } from '../common.js';
|
|
4
|
+
import { Buffer } from 'buffer';
|
|
4
5
|
|
|
5
|
-
const filepath
|
|
6
|
-
const expected
|
|
6
|
+
const filepath = 'x.txt';
|
|
7
|
+
const expected = 'xyz\n';
|
|
7
8
|
|
|
8
9
|
suite('read', () => {
|
|
9
10
|
test('read file asynchronously', async () => {
|
|
10
11
|
const handle = await fs.promises.open(filepath, 'r');
|
|
11
12
|
const { bytesRead, buffer } = await handle.read(Buffer.alloc(expected.length), 0, expected.length, 0);
|
|
12
|
-
|
|
13
13
|
assert.equal(bytesRead, expected.length);
|
|
14
|
-
assert(buffer.toString()
|
|
14
|
+
assert.equal(buffer.toString(), expected);
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
test('read file synchronously', () => {
|
|
@@ -20,19 +20,19 @@ suite('read', () => {
|
|
|
20
20
|
const bytesRead = fs.readSync(fd, buffer, 0, expected.length, 0);
|
|
21
21
|
|
|
22
22
|
assert.equal(bytesRead, expected.length);
|
|
23
|
-
assert(buffer.toString()
|
|
23
|
+
assert.equal(buffer.toString(), expected);
|
|
24
24
|
});
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
suite('read binary', () => {
|
|
28
28
|
test('Read a file and check its binary bytes (asynchronous)', async () => {
|
|
29
29
|
const buff = await fs.promises.readFile('elipses.txt');
|
|
30
|
-
assert((
|
|
30
|
+
assert.strictEqual((buff[1] << 8) | buff[0], 32994);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
test('Read a file and check its binary bytes (synchronous)', () => {
|
|
34
34
|
const buff = fs.readFileSync('elipses.txt');
|
|
35
|
-
assert((
|
|
35
|
+
assert.strictEqual((buff[1] << 8) | buff[0], 32994);
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
38
|
|
|
@@ -45,14 +45,14 @@ suite('read buffer', () => {
|
|
|
45
45
|
const { bytesRead } = await handle.read(bufferAsync, 0, expected.length, 0);
|
|
46
46
|
|
|
47
47
|
assert.strictEqual(bytesRead, expected.length);
|
|
48
|
-
assert(bufferAsync.toString()
|
|
48
|
+
assert.strictEqual(bufferAsync.toString(), expected);
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
test('read file synchronously', () => {
|
|
52
52
|
const fd = fs.openSync(filepath, 'r');
|
|
53
53
|
const bytesRead = fs.readSync(fd, bufferSync, 0, expected.length, 0);
|
|
54
54
|
|
|
55
|
-
assert(bufferSync.toString()
|
|
55
|
+
assert.strictEqual(bufferSync.toString(), expected);
|
|
56
56
|
assert.strictEqual(bytesRead, expected.length);
|
|
57
57
|
});
|
|
58
58
|
|
|
@@ -61,7 +61,7 @@ suite('read buffer', () => {
|
|
|
61
61
|
const buffer = Buffer.alloc(expected.length + 10);
|
|
62
62
|
const bytesRead = fs.readSync(fd, buffer, 10, expected.length, 0);
|
|
63
63
|
|
|
64
|
-
assert(buffer.subarray(10, buffer.length).toString()
|
|
64
|
+
assert.strictEqual(buffer.subarray(10, buffer.length).toString(), expected);
|
|
65
65
|
assert.strictEqual(bytesRead, expected.length);
|
|
66
66
|
});
|
|
67
67
|
});
|
package/tests/fs/times.test.ts
CHANGED
|
@@ -69,7 +69,7 @@ suite('times', () => {
|
|
|
69
69
|
test('read changes atime', async () => {
|
|
70
70
|
const before = fs.statSync(path).atimeMs;
|
|
71
71
|
fs.readFileSync(path);
|
|
72
|
-
await wait(
|
|
72
|
+
await wait(25);
|
|
73
73
|
const after = fs.statSync(path).atimeMs;
|
|
74
74
|
assert(before < after);
|
|
75
75
|
});
|
|
@@ -77,7 +77,7 @@ suite('times', () => {
|
|
|
77
77
|
test('write changes mtime', async () => {
|
|
78
78
|
const before = fs.statSync(path).mtimeMs;
|
|
79
79
|
fs.writeFileSync(path, 'cool');
|
|
80
|
-
await wait(
|
|
80
|
+
await wait(25);
|
|
81
81
|
const after = fs.statSync(path).mtimeMs;
|
|
82
82
|
assert(before < after);
|
|
83
83
|
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path/posix';
|
|
3
|
+
import { configureSingle, InMemory, InMemoryStore, mounts, Overlay, Readonly, resolveMountConfig, StoreFS } from '../../dist/index.js';
|
|
4
|
+
import { S_IFDIR } from '../../dist/vfs/constants.js';
|
|
5
|
+
import { copy, data } from '../setup.js';
|
|
6
|
+
|
|
7
|
+
copy(data);
|
|
8
|
+
|
|
9
|
+
const index = (mounts.get('/') as StoreFS).createIndexSync();
|
|
10
|
+
|
|
11
|
+
class MockFS extends Readonly(StoreFS) {
|
|
12
|
+
constructor() {
|
|
13
|
+
super(new InMemoryStore());
|
|
14
|
+
this.loadIndexSync(index);
|
|
15
|
+
|
|
16
|
+
using tx = this.store.transaction();
|
|
17
|
+
|
|
18
|
+
for (const [path, node] of index) {
|
|
19
|
+
if (node.mode & S_IFDIR) continue;
|
|
20
|
+
|
|
21
|
+
const content = readFileSync(join(data, path));
|
|
22
|
+
|
|
23
|
+
tx.setSync(node.data, content);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
tx.commitSync();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const readable = new MockFS();
|
|
31
|
+
|
|
32
|
+
await readable.ready();
|
|
33
|
+
|
|
34
|
+
await configureSingle({
|
|
35
|
+
backend: Overlay,
|
|
36
|
+
readable,
|
|
37
|
+
writable: await resolveMountConfig({ backend: InMemory, name: 'cow' }),
|
|
38
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { MessageChannel } from 'node:worker_threads';
|
|
2
|
+
import { InMemory, Port, configureSingle, fs, resolveMountConfig, resolveRemoteMount } from '../../dist/index.js';
|
|
3
|
+
import { copy, data } from '../setup.js';
|
|
4
|
+
|
|
5
|
+
const { port1: localPort, port2: remotePort } = new MessageChannel();
|
|
6
|
+
|
|
7
|
+
fs.umount('/');
|
|
8
|
+
const tmpfs = await resolveMountConfig({ backend: InMemory, name: 'tmp' });
|
|
9
|
+
|
|
10
|
+
fs.mount('/', tmpfs);
|
|
11
|
+
copy(data, fs);
|
|
12
|
+
|
|
13
|
+
await resolveRemoteMount(remotePort, tmpfs);
|
|
14
|
+
|
|
15
|
+
await configureSingle({ backend: Port, port: localPort });
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { NoSyncFile } from '../file.js';
|
|
2
|
-
import { FileSystem } from '../filesystem.js';
|
|
3
|
-
import type { StatsLike } from '../stats.js';
|
|
4
|
-
import { Stats } from '../stats.js';
|
|
5
|
-
/**
|
|
6
|
-
* An Index in JSON form
|
|
7
|
-
* @internal
|
|
8
|
-
*/
|
|
9
|
-
export interface IndexData {
|
|
10
|
-
version: number;
|
|
11
|
-
entries: Record<string, StatsLike<number>>;
|
|
12
|
-
}
|
|
13
|
-
export declare const version = 1;
|
|
14
|
-
/**
|
|
15
|
-
* An index of files
|
|
16
|
-
* @internal
|
|
17
|
-
*/
|
|
18
|
-
export declare class Index extends Map<string, Stats> {
|
|
19
|
-
/**
|
|
20
|
-
* Convenience method
|
|
21
|
-
*/
|
|
22
|
-
files(): Map<string, Stats>;
|
|
23
|
-
/**
|
|
24
|
-
* Converts the index to JSON
|
|
25
|
-
*/
|
|
26
|
-
toJSON(): IndexData;
|
|
27
|
-
/**
|
|
28
|
-
* Converts the index to a string
|
|
29
|
-
*/
|
|
30
|
-
toString(): string;
|
|
31
|
-
/**
|
|
32
|
-
* Returns the files in the directory `dir`.
|
|
33
|
-
* This is expensive so it is only called once per directory.
|
|
34
|
-
*/
|
|
35
|
-
protected dirEntries(dir: string): string[];
|
|
36
|
-
/**
|
|
37
|
-
* Loads the index from JSON data
|
|
38
|
-
*/
|
|
39
|
-
fromJSON(json: IndexData): void;
|
|
40
|
-
/**
|
|
41
|
-
* Parses an index from a string
|
|
42
|
-
*/
|
|
43
|
-
static parse(data: string): Index;
|
|
44
|
-
}
|
|
45
|
-
declare const IndexFS_base: import("../index.js").Mixin<typeof FileSystem, import("../mixins/readonly.js").ReadonlyMixin>;
|
|
46
|
-
export declare abstract class IndexFS extends IndexFS_base {
|
|
47
|
-
private indexData;
|
|
48
|
-
protected index: Index;
|
|
49
|
-
protected _isInitialized: boolean;
|
|
50
|
-
ready(): Promise<void>;
|
|
51
|
-
constructor(indexData: IndexData | Promise<IndexData>);
|
|
52
|
-
reloadFiles(): Promise<void>;
|
|
53
|
-
reloadFilesSync(): void;
|
|
54
|
-
stat(path: string): Promise<Stats>;
|
|
55
|
-
statSync(path: string): Stats;
|
|
56
|
-
openFile(path: string, flag: string): Promise<NoSyncFile<this>>;
|
|
57
|
-
openFileSync(path: string, flag: string): NoSyncFile<this>;
|
|
58
|
-
readdir(path: string): Promise<string[]>;
|
|
59
|
-
readdirSync(path: string): string[];
|
|
60
|
-
protected abstract getData(path: string, stats: Stats): Promise<Uint8Array>;
|
|
61
|
-
protected abstract getDataSync(path: string, stats: Stats): Uint8Array;
|
|
62
|
-
}
|
|
63
|
-
export {};
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
/* Note: this file is named file_index.ts because Typescript has special behavior regarding index.ts which can't be disabled. */
|
|
2
|
-
import { isJSON } from 'utilium';
|
|
3
|
-
import { Errno, ErrnoError } from '../error.js';
|
|
4
|
-
import { NoSyncFile, isWriteable } from '../file.js';
|
|
5
|
-
import { FileSystem } from '../filesystem.js';
|
|
6
|
-
import { Readonly } from '../mixins/readonly.js';
|
|
7
|
-
import { Stats } from '../stats.js';
|
|
8
|
-
import { decodeUTF8, encodeUTF8 } from '../utils.js';
|
|
9
|
-
import { basename, dirname } from '../vfs/path.js';
|
|
10
|
-
export const version = 1;
|
|
11
|
-
/**
|
|
12
|
-
* An index of files
|
|
13
|
-
* @internal
|
|
14
|
-
*/
|
|
15
|
-
export class Index extends Map {
|
|
16
|
-
/**
|
|
17
|
-
* Convenience method
|
|
18
|
-
*/
|
|
19
|
-
files() {
|
|
20
|
-
const files = new Map();
|
|
21
|
-
for (const [path, stats] of this) {
|
|
22
|
-
if (stats.isFile()) {
|
|
23
|
-
files.set(path, stats);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return files;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Converts the index to JSON
|
|
30
|
-
*/
|
|
31
|
-
toJSON() {
|
|
32
|
-
return {
|
|
33
|
-
version,
|
|
34
|
-
entries: Object.fromEntries(this),
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Converts the index to a string
|
|
39
|
-
*/
|
|
40
|
-
toString() {
|
|
41
|
-
return JSON.stringify(this.toJSON());
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Returns the files in the directory `dir`.
|
|
45
|
-
* This is expensive so it is only called once per directory.
|
|
46
|
-
*/
|
|
47
|
-
dirEntries(dir) {
|
|
48
|
-
const entries = [];
|
|
49
|
-
for (const entry of this.keys()) {
|
|
50
|
-
if (dirname(entry) == dir) {
|
|
51
|
-
entries.push(basename(entry));
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return entries;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Loads the index from JSON data
|
|
58
|
-
*/
|
|
59
|
-
fromJSON(json) {
|
|
60
|
-
if (json.version != version) {
|
|
61
|
-
throw new ErrnoError(Errno.EINVAL, 'Index version mismatch');
|
|
62
|
-
}
|
|
63
|
-
this.clear();
|
|
64
|
-
for (const [path, data] of Object.entries(json.entries)) {
|
|
65
|
-
const stats = new Stats(data);
|
|
66
|
-
if (stats.isDirectory()) {
|
|
67
|
-
stats.fileData = encodeUTF8(JSON.stringify(this.dirEntries(path)));
|
|
68
|
-
}
|
|
69
|
-
this.set(path, stats);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Parses an index from a string
|
|
74
|
-
*/
|
|
75
|
-
static parse(data) {
|
|
76
|
-
if (!isJSON(data)) {
|
|
77
|
-
throw new ErrnoError(Errno.EINVAL, 'Invalid JSON');
|
|
78
|
-
}
|
|
79
|
-
const json = JSON.parse(data);
|
|
80
|
-
const index = new Index();
|
|
81
|
-
index.fromJSON(json);
|
|
82
|
-
return index;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
export class IndexFS extends Readonly(FileSystem) {
|
|
86
|
-
async ready() {
|
|
87
|
-
await super.ready();
|
|
88
|
-
if (this._isInitialized) {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
this.index.fromJSON(await this.indexData);
|
|
92
|
-
this._isInitialized = true;
|
|
93
|
-
}
|
|
94
|
-
constructor(indexData) {
|
|
95
|
-
super();
|
|
96
|
-
this.indexData = indexData;
|
|
97
|
-
this.index = new Index();
|
|
98
|
-
this._isInitialized = false;
|
|
99
|
-
}
|
|
100
|
-
async reloadFiles() {
|
|
101
|
-
for (const [path, stats] of this.index.files()) {
|
|
102
|
-
delete stats.fileData;
|
|
103
|
-
stats.fileData = await this.getData(path, stats);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
reloadFilesSync() {
|
|
107
|
-
for (const [path, stats] of this.index.files()) {
|
|
108
|
-
delete stats.fileData;
|
|
109
|
-
stats.fileData = this.getDataSync(path, stats);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
stat(path) {
|
|
113
|
-
return Promise.resolve(this.statSync(path));
|
|
114
|
-
}
|
|
115
|
-
statSync(path) {
|
|
116
|
-
if (!this.index.has(path)) {
|
|
117
|
-
throw ErrnoError.With('ENOENT', path, 'stat');
|
|
118
|
-
}
|
|
119
|
-
return this.index.get(path);
|
|
120
|
-
}
|
|
121
|
-
async openFile(path, flag) {
|
|
122
|
-
if (isWriteable(flag)) {
|
|
123
|
-
// You can't write to files on this file system.
|
|
124
|
-
throw new ErrnoError(Errno.EPERM, path);
|
|
125
|
-
}
|
|
126
|
-
// Check if the path exists, and is a file.
|
|
127
|
-
const stats = this.index.get(path);
|
|
128
|
-
if (!stats) {
|
|
129
|
-
throw ErrnoError.With('ENOENT', path, 'openFile');
|
|
130
|
-
}
|
|
131
|
-
return new NoSyncFile(this, path, flag, stats, stats.isDirectory() ? stats.fileData : await this.getData(path, stats));
|
|
132
|
-
}
|
|
133
|
-
openFileSync(path, flag) {
|
|
134
|
-
if (isWriteable(flag)) {
|
|
135
|
-
// You can't write to files on this file system.
|
|
136
|
-
throw new ErrnoError(Errno.EPERM, path);
|
|
137
|
-
}
|
|
138
|
-
// Check if the path exists, and is a file.
|
|
139
|
-
const stats = this.index.get(path);
|
|
140
|
-
if (!stats) {
|
|
141
|
-
throw ErrnoError.With('ENOENT', path, 'openFile');
|
|
142
|
-
}
|
|
143
|
-
return new NoSyncFile(this, path, flag, stats, stats.isDirectory() ? stats.fileData : this.getDataSync(path, stats));
|
|
144
|
-
}
|
|
145
|
-
readdir(path) {
|
|
146
|
-
return Promise.resolve(this.readdirSync(path));
|
|
147
|
-
}
|
|
148
|
-
readdirSync(path) {
|
|
149
|
-
// Check if it exists.
|
|
150
|
-
const stats = this.index.get(path);
|
|
151
|
-
if (!stats) {
|
|
152
|
-
throw ErrnoError.With('ENOENT', path, 'readdir');
|
|
153
|
-
}
|
|
154
|
-
const content = JSON.parse(decodeUTF8(stats.fileData));
|
|
155
|
-
if (!Array.isArray(content)) {
|
|
156
|
-
throw ErrnoError.With('ENODATA', path, 'readdir');
|
|
157
|
-
}
|
|
158
|
-
if (!content.every(item => typeof item == 'string')) {
|
|
159
|
-
throw ErrnoError.With('ENODATA', path, 'readdir');
|
|
160
|
-
}
|
|
161
|
-
return content;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert';
|
|
2
|
-
import { suite, test } from 'node:test';
|
|
3
|
-
import { configure } from '../../dist/config.js';
|
|
4
|
-
import * as fs from '../../dist/vfs/index.js';
|
|
5
|
-
import { InMemoryStore, StoreFS } from '../../dist/index.js';
|
|
6
|
-
import { Async } from '../../dist/mixins/async.js';
|
|
7
|
-
|
|
8
|
-
class ExampleAsyncFS extends Async(StoreFS) {
|
|
9
|
-
_sync = new StoreFS(new InMemoryStore('cache'));
|
|
10
|
-
|
|
11
|
-
public constructor() {
|
|
12
|
-
super(new InMemoryStore('test'));
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const asyncFS = new ExampleAsyncFS();
|
|
17
|
-
|
|
18
|
-
await configure({ mounts: { '/': asyncFS } });
|
|
19
|
-
|
|
20
|
-
suite('Async Mixin', () => {
|
|
21
|
-
test('async -> cache syncing', async () => {
|
|
22
|
-
await fs.promises.writeFile('test', 'test');
|
|
23
|
-
assert.strictEqual(fs.readFileSync('test', 'utf8'), 'test');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test('cache -> async syncing', async () => {
|
|
27
|
-
fs.unlinkSync('test');
|
|
28
|
-
await asyncFS.queueDone();
|
|
29
|
-
assert(!(await fs.promises.exists('test')));
|
|
30
|
-
});
|
|
31
|
-
});
|
package/tests/setup/cow+fetch.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { createServer } from 'node:http';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { configureSingle, Fetch, InMemory, Overlay } from '../../dist/index.js';
|
|
6
|
-
import { data, tmp } from '../setup.js';
|
|
7
|
-
|
|
8
|
-
const port = 26514,
|
|
9
|
-
index = tmp + '/index.json';
|
|
10
|
-
|
|
11
|
-
const statusCodes = {
|
|
12
|
-
ENOENT: 404,
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
execSync(`npm exec make-index -- ${data} --output ${index} --quiet`, { stdio: 'inherit' });
|
|
16
|
-
|
|
17
|
-
const server = createServer((request, response) => {
|
|
18
|
-
const path = request.url == '/.index.json' ? index : join(data, request.url?.slice(1) || '');
|
|
19
|
-
try {
|
|
20
|
-
response.statusCode = 200;
|
|
21
|
-
response.end(readFileSync(path));
|
|
22
|
-
} catch (e: any) {
|
|
23
|
-
response.statusCode = statusCodes[e.code as keyof typeof statusCodes] || 400;
|
|
24
|
-
response.end();
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
server
|
|
29
|
-
.once('error', (error: NodeJS.ErrnoException) => {
|
|
30
|
-
if (error.code == 'EADDRINUSE') return;
|
|
31
|
-
throw error;
|
|
32
|
-
})
|
|
33
|
-
.listen(port)
|
|
34
|
-
.unref();
|
|
35
|
-
|
|
36
|
-
const baseUrl = 'http://localhost:' + port;
|
|
37
|
-
|
|
38
|
-
await configureSingle({
|
|
39
|
-
backend: Overlay,
|
|
40
|
-
readable: Fetch.create({
|
|
41
|
-
baseUrl,
|
|
42
|
-
index: baseUrl + '/.index.json',
|
|
43
|
-
}),
|
|
44
|
-
writable: InMemory.create({ name: 'cow' }),
|
|
45
|
-
});
|
|
File without changes
|