@zenfs/core 2.6.5 → 2.7.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/cow.js +4 -3
- package/dist/backends/fetch.d.ts +4 -0
- package/dist/backends/fetch.js +30 -1
- package/dist/backends/memory.d.ts +1 -1
- package/dist/backends/store/fs.js +18 -5
- package/dist/config.js +7 -7
- package/dist/internal/contexts.d.ts +4 -0
- package/dist/internal/contexts.js +10 -6
- package/dist/internal/file_index.js +2 -1
- package/dist/internal/filesystem.d.ts +5 -0
- package/dist/internal/filesystem.js +18 -0
- package/dist/internal/inode.d.ts +6 -1
- package/dist/internal/inode.js +12 -3
- package/dist/internal/ioctl.d.ts +148 -0
- package/dist/internal/ioctl.js +368 -0
- package/dist/mixins/async.js +6 -1
- package/dist/mixins/mutexed.d.ts +3 -0
- package/dist/mixins/mutexed.js +29 -1
- package/dist/mixins/shared.d.ts +1 -1
- package/dist/mixins/shared.js +1 -0
- package/dist/mixins/sync.js +3 -0
- package/dist/node/sync.js +8 -8
- package/dist/vfs/async.js +2 -2
- package/dist/vfs/file.js +2 -1
- package/dist/vfs/ioctl.d.ts +8 -85
- package/dist/vfs/ioctl.js +67 -369
- package/dist/vfs/shared.d.ts +2 -0
- package/dist/vfs/shared.js +12 -8
- package/dist/vfs/sync.d.ts +1 -1
- package/dist/vfs/sync.js +11 -11
- package/dist/vfs/vcache.js +3 -0
- package/package.json +10 -1
- package/scripts/make-index.js +22 -7
- package/tests/backend/cow.test.ts +33 -0
- package/tests/backend/fetch.test.ts +8 -0
- package/tests/fs/truncate.test.ts +22 -0
package/dist/vfs/sync.js
CHANGED
|
@@ -113,11 +113,11 @@ export function resolve($, path, preserveSymlinks, extra) {
|
|
|
113
113
|
* @category VFS
|
|
114
114
|
* @internal
|
|
115
115
|
*/
|
|
116
|
-
export function open(path, opt) {
|
|
116
|
+
export function open($, path, opt) {
|
|
117
117
|
path = normalizePath(path);
|
|
118
118
|
const mode = normalizeMode(opt.mode, 0o644), flag = flags.parse(opt.flag);
|
|
119
|
-
path = opt.preserveSymlinks ? path : resolve(
|
|
120
|
-
const { fs, path: resolved } = resolveMount(path,
|
|
119
|
+
path = opt.preserveSymlinks ? path : resolve($, path).fullPath;
|
|
120
|
+
const { fs, path: resolved } = resolveMount(path, $);
|
|
121
121
|
let stats;
|
|
122
122
|
try {
|
|
123
123
|
stats = fs.statSync(resolved);
|
|
@@ -134,25 +134,25 @@ export function open(path, opt) {
|
|
|
134
134
|
// Create the file
|
|
135
135
|
const parentPath = dirname(resolved);
|
|
136
136
|
const parentStats = fs.statSync(parentPath);
|
|
137
|
-
if (checkAccess && !hasAccess(
|
|
137
|
+
if (checkAccess && !hasAccess($, parentStats, constants.W_OK)) {
|
|
138
138
|
throw UV('EACCES', 'open', path);
|
|
139
139
|
}
|
|
140
140
|
if (!isDirectory(parentStats)) {
|
|
141
141
|
throw UV('ENOTDIR', 'open', path);
|
|
142
142
|
}
|
|
143
|
-
if (!opt.allowDirectory && mode
|
|
143
|
+
if (!opt.allowDirectory && isDirectory({ mode }))
|
|
144
144
|
throw UV('EISDIR', 'open', path);
|
|
145
145
|
// Serialize entry creation with other operations on the parent directory
|
|
146
146
|
const _ = __addDisposableResource(env_1, lockPathSync(fs, parentPath, 'rw', parentStats), false);
|
|
147
|
-
const { euid: uid, egid: gid } = contextOf(
|
|
147
|
+
const { euid: uid, egid: gid } = contextOf($).credentials;
|
|
148
148
|
const inode = fs.createFileSync(resolved, {
|
|
149
149
|
mode,
|
|
150
150
|
uid: parentStats.mode & constants.S_ISUID ? parentStats.uid : uid,
|
|
151
151
|
gid: parentStats.mode & constants.S_ISGID ? parentStats.gid : gid,
|
|
152
152
|
});
|
|
153
153
|
// A new entry in the parent directory, which is a 'rename' event
|
|
154
|
-
emitChange(
|
|
155
|
-
return new Handle(
|
|
154
|
+
emitChange($, 'rename', path);
|
|
155
|
+
return new Handle($, path, resolved, flag, cacheOf(fs).ref(resolved, inode));
|
|
156
156
|
}
|
|
157
157
|
catch (e_1) {
|
|
158
158
|
env_1.error = e_1;
|
|
@@ -162,14 +162,14 @@ export function open(path, opt) {
|
|
|
162
162
|
__disposeResources(env_1);
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
|
-
if (checkAccess && (!hasAccess(
|
|
165
|
+
if (checkAccess && (!hasAccess($, stats, mode) || !hasAccess($, stats, flags.toMode(flag)))) {
|
|
166
166
|
throw UV('EACCES', 'open', path);
|
|
167
167
|
}
|
|
168
168
|
if (flag & constants.O_EXCL)
|
|
169
169
|
throw UV('EEXIST', 'open', path);
|
|
170
|
-
if (!opt.allowDirectory && stats
|
|
170
|
+
if (!opt.allowDirectory && isDirectory(stats))
|
|
171
171
|
throw UV('EISDIR', 'open', path);
|
|
172
|
-
const file = new Handle(
|
|
172
|
+
const file = new Handle($, path, resolved, flag, cacheOf(fs).ref(resolved, stats));
|
|
173
173
|
if (flag & constants.O_TRUNC)
|
|
174
174
|
file.truncateSync(0);
|
|
175
175
|
return file;
|
package/dist/vfs/vcache.js
CHANGED
|
@@ -50,6 +50,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
50
50
|
var e = new Error(message);
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
|
+
import { err } from 'kerium/log';
|
|
53
54
|
import { VNode } from './vnode.js';
|
|
54
55
|
/**
|
|
55
56
|
* The VFS-level cache of vnodes for a single file system. Modeled after Linux's dcache/icache.
|
|
@@ -76,6 +77,8 @@ export class VCache {
|
|
|
76
77
|
*/
|
|
77
78
|
ref(path, inode) {
|
|
78
79
|
let node = this.byIno.get(inode.ino);
|
|
80
|
+
if (node && node.inode.nlink === node.paths.size && !node.paths.has(path))
|
|
81
|
+
err(`vcache.ref: vnode for ${this.fs.label || this.fs.uuid}:${inode.ino} has an nlink of ${node.inode.nlink} but referenced by more paths [#314]`);
|
|
79
82
|
if (!node) {
|
|
80
83
|
node = new VNode(this.fs, path, inode);
|
|
81
84
|
this.byIno.set(inode.ino, node);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenfs/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "A filesystem, anywhere",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -93,5 +93,14 @@
|
|
|
93
93
|
"typedoc": "^0.28.18",
|
|
94
94
|
"typescript": "^6.0.0",
|
|
95
95
|
"typescript-eslint": "^8.58.0"
|
|
96
|
+
},
|
|
97
|
+
"prettier": {
|
|
98
|
+
"singleQuote": true,
|
|
99
|
+
"useTabs": true,
|
|
100
|
+
"trailingComma": "es5",
|
|
101
|
+
"tabWidth": 4,
|
|
102
|
+
"printWidth": 150,
|
|
103
|
+
"arrowParens": "avoid",
|
|
104
|
+
"experimentalOperatorPosition": "start"
|
|
96
105
|
}
|
|
97
106
|
}
|
package/scripts/make-index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { lstatSync, readdirSync, writeFileSync
|
|
2
|
+
import { constants as c, lstatSync, readdirSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join, matchesGlob, relative, resolve } from 'node:path/posix';
|
|
4
4
|
import { parseArgs, styleText } from 'node:util';
|
|
5
5
|
|
|
@@ -9,6 +9,7 @@ const { values: options, positionals } = parseArgs({
|
|
|
9
9
|
ignore: { short: 'i', type: 'string', multiple: true, default: [] },
|
|
10
10
|
output: { short: 'o', type: 'string', default: 'index.json' },
|
|
11
11
|
quiet: { short: 'q', type: 'boolean', default: false },
|
|
12
|
+
xdev: { type: 'boolean', default: false },
|
|
12
13
|
verbose: { type: 'boolean', default: false },
|
|
13
14
|
},
|
|
14
15
|
allowPositionals: true,
|
|
@@ -22,11 +23,12 @@ if (options.help) {
|
|
|
22
23
|
path: The path to create a listing for
|
|
23
24
|
|
|
24
25
|
options:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
-h, --help Outputs this help message
|
|
27
|
+
-q, --quiet The command will not generate any output, including error messages.
|
|
28
|
+
--verbose Output verbose messages
|
|
29
|
+
--xdev Force crossing device/FS boundaries
|
|
30
|
+
-o, --output <path> Path to the output file. Defaults to listing.
|
|
31
|
+
-i, --ignore <pattern> Ignores files which match the glob <pattern>. Can be passed multiple times.
|
|
30
32
|
`);
|
|
31
33
|
process.exit();
|
|
32
34
|
}
|
|
@@ -75,6 +77,8 @@ function getTypeText(/** @type {{ mode: number }} */ stats) {
|
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
const seenDevs = new Set();
|
|
81
|
+
|
|
78
82
|
/**
|
|
79
83
|
* @param {string} path
|
|
80
84
|
*/
|
|
@@ -86,6 +90,17 @@ function computeEntries(path) {
|
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
const stats = lstatSync(path);
|
|
93
|
+
if (!seenDevs.size) seenDevs.add(stats.dev);
|
|
94
|
+
else if (!seenDevs.has(stats.dev))
|
|
95
|
+
if (options.xdev) {
|
|
96
|
+
console.warn(
|
|
97
|
+
`${styleText('yellowBright', `--xdev: Adding entries from device ${stats.dev} (${path}). You may get duplicate inos which MUST be de-duplicated manually.`)}`
|
|
98
|
+
);
|
|
99
|
+
seenDevs.add(stats.dev);
|
|
100
|
+
} else {
|
|
101
|
+
console.warn(`${styleText('yellowBright', `Ignoring ${path} because it crosses a device boundary`)}`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
89
104
|
|
|
90
105
|
const type = getTypeText(stats);
|
|
91
106
|
|
|
@@ -97,7 +112,7 @@ function computeEntries(path) {
|
|
|
97
112
|
}
|
|
98
113
|
} catch (/** @type {any} */ e) {
|
|
99
114
|
if (!options.quiet) {
|
|
100
|
-
console.log(`${styleText('red', 'fail')} ${path}: ${e.message}`);
|
|
115
|
+
console.log(`${styleText('red', 'fail')} ${path}: ${e instanceof Error ? e.message : String(e)}`);
|
|
101
116
|
}
|
|
102
117
|
}
|
|
103
118
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
import { CopyOnWriteFS, InMemoryStore, StoreFS } from '@zenfs/core';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { suite, test } from 'node:test';
|
|
5
|
+
import { setupLogs } from '../logs.js';
|
|
6
|
+
setupLogs();
|
|
7
|
+
|
|
8
|
+
const size = 1 << 20;
|
|
9
|
+
|
|
10
|
+
const options = { mode: 0o644, uid: 0, gid: 0 };
|
|
11
|
+
|
|
12
|
+
suite('CopyOnWrite', () => {
|
|
13
|
+
test('Truncating a base-layer file does not copy up the bytes it discards #313', async () => {
|
|
14
|
+
const readable = new StoreFS(new InMemoryStore(undefined, 'ro'));
|
|
15
|
+
await readable.ready();
|
|
16
|
+
await readable.createFile('/big', options);
|
|
17
|
+
await readable.write('/big', new Uint8Array(size).fill(0x41), 0);
|
|
18
|
+
|
|
19
|
+
const writable = new StoreFS(new InMemoryStore(undefined, 'rw'));
|
|
20
|
+
await writable.ready();
|
|
21
|
+
|
|
22
|
+
const fs = new CopyOnWriteFS(readable, writable);
|
|
23
|
+
await fs.touch('/big', { ...(await fs.stat('/big')), size: 4 });
|
|
24
|
+
|
|
25
|
+
assert.equal((await fs.stat('/big')).size, 4);
|
|
26
|
+
|
|
27
|
+
await using tx = writable.transaction();
|
|
28
|
+
const data = (await tx.get((await writable.stat('/big')).data))!;
|
|
29
|
+
|
|
30
|
+
assert.equal(data.byteLength, 4);
|
|
31
|
+
assert.equal(data.buffer.byteLength, 4);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -68,6 +68,14 @@ suite('CopyOnWrite over uncached Fetch #301', () => {
|
|
|
68
68
|
// Writes go through the vnode cache, so this surfaces when the handle is flushed rather than at `write`.
|
|
69
69
|
assert.throws(() => fs.fsyncSync(fd), { code: 'EAGAIN' });
|
|
70
70
|
});
|
|
71
|
+
|
|
72
|
+
test('Contents follow the size across the layers #313', async () => {
|
|
73
|
+
await fs.promises.truncate('/cow/a1.js', 4);
|
|
74
|
+
assert.equal(await fs.promises.readFile('/cow/a1.js', 'utf8'), '// C');
|
|
75
|
+
|
|
76
|
+
await fs.promises.truncate('/cow/a1.js', 10);
|
|
77
|
+
assert.equal(await fs.promises.readFile('/cow/a1.js', 'utf8'), '// C\0\0\0\0\0\0');
|
|
78
|
+
});
|
|
71
79
|
});
|
|
72
80
|
|
|
73
81
|
after(async () => {
|
|
@@ -34,6 +34,17 @@ suite('Truncating', config('truncate'), () => {
|
|
|
34
34
|
fs.closeSync(fd);
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
test('Contents follow the size #313', config('sync'), () => {
|
|
38
|
+
const path = 'truncate-contents.txt';
|
|
39
|
+
fs.writeFileSync(path, 'SECRETDATA');
|
|
40
|
+
|
|
41
|
+
fs.truncateSync(path, 4);
|
|
42
|
+
assert.equal(fs.readFileSync(path, 'utf8'), 'SECR');
|
|
43
|
+
|
|
44
|
+
fs.truncateSync(path, 10);
|
|
45
|
+
assert.equal(fs.readFileSync(path, 'utf8'), 'SECR\0\0\0\0\0\0');
|
|
46
|
+
});
|
|
47
|
+
|
|
37
48
|
const statSize = async (path: string) => (await fs.promises.stat(path)).size;
|
|
38
49
|
|
|
39
50
|
test('Async path functions', config('async'), async () => {
|
|
@@ -64,4 +75,15 @@ suite('Truncating', config('truncate'), () => {
|
|
|
64
75
|
|
|
65
76
|
await handle.close();
|
|
66
77
|
});
|
|
78
|
+
|
|
79
|
+
test('Contents follow the size (async) #313', config('async'), async () => {
|
|
80
|
+
const path = 'truncate-contents-async.txt';
|
|
81
|
+
await fs.promises.writeFile(path, 'SECRETDATA');
|
|
82
|
+
|
|
83
|
+
await fs.promises.truncate(path, 4);
|
|
84
|
+
assert.equal(await fs.promises.readFile(path, 'utf8'), 'SECR');
|
|
85
|
+
|
|
86
|
+
await fs.promises.truncate(path, 10);
|
|
87
|
+
assert.equal(await fs.promises.readFile(path, 'utf8'), 'SECR\0\0\0\0\0\0');
|
|
88
|
+
});
|
|
67
89
|
});
|