@zenfs/core 1.9.4 → 1.9.5
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/memory.d.ts +0 -1
- package/dist/backends/memory.js +0 -3
- package/dist/backends/port/fs.js +1 -1
- package/dist/backends/port/rpc.d.ts +6 -2
- package/dist/backends/port/rpc.js +15 -4
- package/dist/utils.d.ts +1 -2
- package/dist/utils.js +10 -2
- package/dist/vfs/promises.js +12 -14
- package/dist/vfs/sync.js +7 -7
- package/package.json +1 -1
- package/scripts/test.js +7 -4
- package/tests/backend/port.test.ts +11 -9
- package/tests/common/path.test.ts +12 -0
- package/tests/fs/links.test.ts +25 -0
|
@@ -10,7 +10,6 @@ export declare class InMemoryStore extends Map<number, Uint8Array> implements Sy
|
|
|
10
10
|
readonly name = "tmpfs";
|
|
11
11
|
constructor(label?: string | undefined);
|
|
12
12
|
sync(): Promise<void>;
|
|
13
|
-
clearSync(): void;
|
|
14
13
|
transaction(): SyncMapTransaction;
|
|
15
14
|
}
|
|
16
15
|
declare const _InMemory: {
|
package/dist/backends/memory.js
CHANGED
package/dist/backends/port/fs.js
CHANGED
|
@@ -145,7 +145,7 @@ export async function resolveRemoteMount(port, config, _depth = 0) {
|
|
|
145
145
|
const stopAndReplay = RPC.catchMessages(port);
|
|
146
146
|
const fs = await resolveMountConfig(config, _depth);
|
|
147
147
|
attachFS(port, fs);
|
|
148
|
-
stopAndReplay(fs);
|
|
148
|
+
await stopAndReplay(fs);
|
|
149
149
|
info('Resolved remote mount: ' + fs.toString());
|
|
150
150
|
return fs;
|
|
151
151
|
}
|
|
@@ -10,7 +10,7 @@ type _MessageEvent<T = any> = T | {
|
|
|
10
10
|
/** @internal */
|
|
11
11
|
export interface Port {
|
|
12
12
|
postMessage(value: unknown, transfer?: TransferListItem[]): void;
|
|
13
|
-
on?(event: 'message', listener: (value: unknown) => void): this;
|
|
13
|
+
on?(event: 'message' | 'online', listener: (value: unknown) => void): this;
|
|
14
14
|
off?(event: 'message', listener: (value: unknown) => void): this;
|
|
15
15
|
addEventListener?(type: 'message', listener: (ev: _MessageEvent) => void): void;
|
|
16
16
|
removeEventListener?(type: 'message', listener: (ev: _MessageEvent) => void): void;
|
|
@@ -70,4 +70,8 @@ export declare function request<const TRequest extends Request, TValue>(request:
|
|
|
70
70
|
export declare function handleResponse<const TResponse extends Response>(response: TResponse): void;
|
|
71
71
|
export declare function attach<T extends Message>(port: Port, handler: (message: T) => unknown): void;
|
|
72
72
|
export declare function detach<T extends Message>(port: Port, handler: (message: T) => unknown): void;
|
|
73
|
-
export declare function catchMessages<T extends Backend>(port: Port): (fs: FilesystemOf<T>) => void
|
|
73
|
+
export declare function catchMessages<T extends Backend>(port: Port): (fs: FilesystemOf<T>) => Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
export declare function waitOnline(port: Port): Promise<void>;
|
|
@@ -64,7 +64,7 @@ export function attach(port, handler) {
|
|
|
64
64
|
throw err(new ErrnoError(Errno.EINVAL, 'Cannot attach to non-existent port'));
|
|
65
65
|
info('Attached handler to port: ' + handler.name);
|
|
66
66
|
port['on' in port ? 'on' : 'addEventListener']('message', (message) => {
|
|
67
|
-
handler('data' in message ? message.data : message);
|
|
67
|
+
handler(typeof message == 'object' && message !== null && 'data' in message ? message.data : message);
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
export function detach(port, handler) {
|
|
@@ -72,18 +72,29 @@ export function detach(port, handler) {
|
|
|
72
72
|
throw err(new ErrnoError(Errno.EINVAL, 'Cannot detach from non-existent port'));
|
|
73
73
|
info('Detached handler from port: ' + handler.name);
|
|
74
74
|
port['off' in port ? 'off' : 'removeEventListener']('message', (message) => {
|
|
75
|
-
handler('data' in message ? message.data : message);
|
|
75
|
+
handler(typeof message == 'object' && message !== null && 'data' in message ? message.data : message);
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
78
|
export function catchMessages(port) {
|
|
79
79
|
const events = [];
|
|
80
80
|
const handler = events.push.bind(events);
|
|
81
81
|
attach(port, handler);
|
|
82
|
-
return function (fs) {
|
|
82
|
+
return async function (fs) {
|
|
83
83
|
detach(port, handler);
|
|
84
84
|
for (const event of events) {
|
|
85
85
|
const request = 'data' in event ? event.data : event;
|
|
86
|
-
|
|
86
|
+
await handleRequest(port, fs, request);
|
|
87
87
|
}
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* @internal
|
|
92
|
+
*/
|
|
93
|
+
export async function waitOnline(port) {
|
|
94
|
+
if (!('on' in port))
|
|
95
|
+
return; // Only need to wait in Node.js
|
|
96
|
+
const online = Promise.withResolvers();
|
|
97
|
+
setTimeout(online.reject, 500);
|
|
98
|
+
port.on('online', online.resolve);
|
|
99
|
+
await online.promise;
|
|
100
|
+
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type * as fs from 'node:fs';
|
|
2
2
|
import type { ClassLike, OptionalTuple } from 'utilium';
|
|
3
3
|
import { ErrnoError } from './internal/error.js';
|
|
4
|
-
import type { AbsolutePath } from './vfs/path.js';
|
|
5
4
|
declare global {
|
|
6
5
|
function atob(data: string): string;
|
|
7
6
|
function btoa(data: string): string;
|
|
@@ -54,7 +53,7 @@ export declare function normalizeTime(time: string | number | Date): number;
|
|
|
54
53
|
* Normalizes a path
|
|
55
54
|
* @internal
|
|
56
55
|
*/
|
|
57
|
-
export declare function normalizePath(p: fs.PathLike):
|
|
56
|
+
export declare function normalizePath(p: fs.PathLike, noResolve?: boolean): string;
|
|
58
57
|
/**
|
|
59
58
|
* Normalizes options
|
|
60
59
|
* @param options options to normalize
|
package/dist/utils.js
CHANGED
|
@@ -101,15 +101,23 @@ export function normalizeTime(time) {
|
|
|
101
101
|
* Normalizes a path
|
|
102
102
|
* @internal
|
|
103
103
|
*/
|
|
104
|
-
export function normalizePath(p) {
|
|
104
|
+
export function normalizePath(p, noResolve = false) {
|
|
105
|
+
if (p instanceof URL) {
|
|
106
|
+
if (p.protocol != 'file:')
|
|
107
|
+
throw new ErrnoError(Errno.EINVAL, 'URLs must use the file: protocol');
|
|
108
|
+
p = p.pathname;
|
|
109
|
+
}
|
|
105
110
|
p = p.toString();
|
|
111
|
+
if (p.startsWith('file://'))
|
|
112
|
+
p = p.slice('file://'.length);
|
|
106
113
|
if (p.includes('\x00')) {
|
|
107
114
|
throw new ErrnoError(Errno.EINVAL, 'Path can not contain null character');
|
|
108
115
|
}
|
|
109
116
|
if (p.length == 0) {
|
|
110
117
|
throw new ErrnoError(Errno.EINVAL, 'Path can not be empty');
|
|
111
118
|
}
|
|
112
|
-
|
|
119
|
+
p = p.replaceAll(/[/\\]+/g, '/');
|
|
120
|
+
return noResolve ? p : resolve(p);
|
|
113
121
|
}
|
|
114
122
|
/**
|
|
115
123
|
* Normalizes options
|
package/dist/vfs/promises.js
CHANGED
|
@@ -89,9 +89,8 @@ export class FileHandle {
|
|
|
89
89
|
*/
|
|
90
90
|
async chmod(mode) {
|
|
91
91
|
const numMode = normalizeMode(mode, -1);
|
|
92
|
-
if (numMode < 0)
|
|
92
|
+
if (numMode < 0)
|
|
93
93
|
throw new ErrnoError(Errno.EINVAL, 'Invalid mode.');
|
|
94
|
-
}
|
|
95
94
|
await this.file.chmod(numMode);
|
|
96
95
|
this._emitChange();
|
|
97
96
|
}
|
|
@@ -487,18 +486,18 @@ async function _open($, path, opt) {
|
|
|
487
486
|
var _a;
|
|
488
487
|
path = normalizePath(path);
|
|
489
488
|
const mode = normalizeMode(opt.mode, 0o644), flag = parseFlag(opt.flag);
|
|
490
|
-
const { fullPath
|
|
489
|
+
const { fullPath, fs, path: resolved, stats } = await _resolve($, path.toString(), opt.preserveSymlinks);
|
|
491
490
|
if (!stats) {
|
|
492
491
|
if ((!isWriteable(flag) && !isAppendable(flag)) || flag == 'r+') {
|
|
493
|
-
throw ErrnoError.With('ENOENT',
|
|
492
|
+
throw ErrnoError.With('ENOENT', fullPath, '_open');
|
|
494
493
|
}
|
|
495
494
|
// Create the file
|
|
496
495
|
const parentStats = await fs.stat(dirname(resolved));
|
|
497
496
|
if (config.checkAccess && !parentStats.hasAccess(constants.W_OK, $)) {
|
|
498
|
-
throw ErrnoError.With('EACCES', dirname(
|
|
497
|
+
throw ErrnoError.With('EACCES', dirname(fullPath), '_open');
|
|
499
498
|
}
|
|
500
499
|
if (!parentStats.isDirectory()) {
|
|
501
|
-
throw ErrnoError.With('ENOTDIR', dirname(
|
|
500
|
+
throw ErrnoError.With('ENOTDIR', dirname(fullPath), '_open');
|
|
502
501
|
}
|
|
503
502
|
const { euid: uid, egid: gid } = (_a = $ === null || $ === void 0 ? void 0 : $.credentials) !== null && _a !== void 0 ? _a : credentials;
|
|
504
503
|
const file = await fs.createFile(resolved, flag, mode, { uid, gid });
|
|
@@ -506,10 +505,10 @@ async function _open($, path, opt) {
|
|
|
506
505
|
return new FileHandle(file, $);
|
|
507
506
|
}
|
|
508
507
|
if (config.checkAccess && !stats.hasAccess(flagToMode(flag), $)) {
|
|
509
|
-
throw ErrnoError.With('EACCES',
|
|
508
|
+
throw ErrnoError.With('EACCES', fullPath, '_open');
|
|
510
509
|
}
|
|
511
510
|
if (isExclusive(flag)) {
|
|
512
|
-
throw ErrnoError.With('EEXIST',
|
|
511
|
+
throw ErrnoError.With('EEXIST', fullPath, '_open');
|
|
513
512
|
}
|
|
514
513
|
const handle = new FileHandle(await fs.openFile(resolved, flag), $);
|
|
515
514
|
/*
|
|
@@ -768,11 +767,11 @@ export async function symlink(target, path, type = 'file') {
|
|
|
768
767
|
if (!['file', 'dir', 'junction'].includes(type)) {
|
|
769
768
|
throw new ErrnoError(Errno.EINVAL, 'Invalid symlink type: ' + type);
|
|
770
769
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
770
|
+
path = normalizePath(path);
|
|
771
|
+
if (await exists.call(this, path))
|
|
772
|
+
throw ErrnoError.With('EEXIST', path, 'symlink');
|
|
774
773
|
const handle = __addDisposableResource(env_5, await _open(this, path, { flag: 'w+', mode: 0o644, preserveSymlinks: true }), true);
|
|
775
|
-
await handle.writeFile(target
|
|
774
|
+
await handle.writeFile(normalizePath(target, true));
|
|
776
775
|
await handle.file.chmod(constants.S_IFLNK);
|
|
777
776
|
}
|
|
778
777
|
catch (e_5) {
|
|
@@ -965,8 +964,7 @@ async function _resolve($, path, preserveSymlinks) {
|
|
|
965
964
|
return { ...resolved, fullPath: maybePath, stats };
|
|
966
965
|
}
|
|
967
966
|
const target = resolve(realDir, (await readlink.call($, maybePath)).toString());
|
|
968
|
-
|
|
969
|
-
return { ...resolved, fullPath: real, stats };
|
|
967
|
+
return await _resolve($, target);
|
|
970
968
|
}
|
|
971
969
|
catch (e) {
|
|
972
970
|
if (e.code == 'ENOENT') {
|
package/dist/vfs/sync.js
CHANGED
|
@@ -235,11 +235,12 @@ openSync;
|
|
|
235
235
|
export function lopenSync(path, flag, mode) {
|
|
236
236
|
return file2fd(_openSync.call(this, path, { flag, mode, preserveSymlinks: true }));
|
|
237
237
|
}
|
|
238
|
-
function _readFileSync(
|
|
238
|
+
function _readFileSync(path, flag, preserveSymlinks) {
|
|
239
239
|
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
240
240
|
try {
|
|
241
|
+
path = normalizePath(path);
|
|
241
242
|
// Get file.
|
|
242
|
-
const file = __addDisposableResource(env_2, _openSync.call(this,
|
|
243
|
+
const file = __addDisposableResource(env_2, _openSync.call(this, path, { flag, mode: 0o644, preserveSymlinks }), false);
|
|
243
244
|
const stat = file.statSync();
|
|
244
245
|
// Allocate buffer.
|
|
245
246
|
const data = new Uint8Array(stat.size);
|
|
@@ -260,7 +261,7 @@ export function readFileSync(path, _options = {}) {
|
|
|
260
261
|
if (!isReadable(flag)) {
|
|
261
262
|
throw new ErrnoError(Errno.EINVAL, 'Flag passed to readFile must allow for reading.');
|
|
262
263
|
}
|
|
263
|
-
const data = Buffer.from(_readFileSync.call(this, typeof path == 'number' ? fd2file(path).path : path
|
|
264
|
+
const data = Buffer.from(_readFileSync.call(this, typeof path == 'number' ? fd2file(path).path : path, options.flag, false));
|
|
264
265
|
return options.encoding ? data.toString(options.encoding) : data;
|
|
265
266
|
}
|
|
266
267
|
readFileSync;
|
|
@@ -564,13 +565,13 @@ export function symlinkSync(target, path, type = 'file') {
|
|
|
564
565
|
if (existsSync.call(this, path)) {
|
|
565
566
|
throw ErrnoError.With('EEXIST', path.toString(), 'symlink');
|
|
566
567
|
}
|
|
567
|
-
writeFileSync.call(this, path, target
|
|
568
|
+
writeFileSync.call(this, path, normalizePath(target, true));
|
|
568
569
|
const file = _openSync.call(this, path, { flag: 'r+', mode: 0o644, preserveSymlinks: true });
|
|
569
570
|
file.chmodSync(constants.S_IFLNK);
|
|
570
571
|
}
|
|
571
572
|
symlinkSync;
|
|
572
573
|
export function readlinkSync(path, options) {
|
|
573
|
-
const value = Buffer.from(_readFileSync.call(this, path
|
|
574
|
+
const value = Buffer.from(_readFileSync.call(this, path, 'r', true));
|
|
574
575
|
const encoding = typeof options == 'object' ? options === null || options === void 0 ? void 0 : options.encoding : options;
|
|
575
576
|
if (encoding == 'buffer') {
|
|
576
577
|
return value;
|
|
@@ -657,8 +658,7 @@ function _resolveSync($, path, preserveSymlinks) {
|
|
|
657
658
|
return { ...resolved, fullPath: maybePath, stats };
|
|
658
659
|
}
|
|
659
660
|
const target = resolve(realDir, readlinkSync.call($, maybePath).toString());
|
|
660
|
-
|
|
661
|
-
return { ...resolved, fullPath: real, stats };
|
|
661
|
+
return _resolveSync($, target);
|
|
662
662
|
}
|
|
663
663
|
catch (e) {
|
|
664
664
|
if (e.code == 'ENOENT') {
|
package/package.json
CHANGED
package/scripts/test.js
CHANGED
|
@@ -51,7 +51,7 @@ Output:
|
|
|
51
51
|
-h, --help Outputs this help message
|
|
52
52
|
-w, --verbose Output verbose messages
|
|
53
53
|
-q, --quiet Don't output normal messages
|
|
54
|
-
|
|
54
|
+
-l, --logs <level> Change the default log level for test output. Level can be a number or string
|
|
55
55
|
-N, --file-names Use full file paths for tests from setup files instead of the base name
|
|
56
56
|
-C, --ci Continuous integration (CI) mode. This interacts with the Github
|
|
57
57
|
Checks API for better test status. Requires @octokit/action
|
|
@@ -166,9 +166,12 @@ if (options.common) {
|
|
|
166
166
|
!options.quiet && console.log('Running common tests...');
|
|
167
167
|
const { pass, fail } = await status('Common tests');
|
|
168
168
|
try {
|
|
169
|
-
execSync(
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
execSync(
|
|
170
|
+
`tsx ${options.inspect ? 'inspect' : ''} ${options.force ? '--test-force-exit' : ''} --test --experimental-test-coverage 'tests/*.test.ts' 'tests/**/!(fs)/*.test.ts'`,
|
|
171
|
+
{
|
|
172
|
+
stdio: ['ignore', options.verbose ? 'inherit' : 'ignore', 'inherit'],
|
|
173
|
+
}
|
|
174
|
+
);
|
|
172
175
|
await pass();
|
|
173
176
|
} catch {
|
|
174
177
|
await fail();
|
|
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
|
|
|
2
2
|
import { suite, test } from 'node:test';
|
|
3
3
|
import { MessageChannel, Worker } from 'node:worker_threads';
|
|
4
4
|
import { Port, attachFS } from '../../dist/backends/port/fs.js';
|
|
5
|
+
import { waitOnline } from '../../dist/backends/port/rpc.js';
|
|
5
6
|
import type { InMemoryStore, StoreFS } from '../../dist/index.js';
|
|
6
7
|
import { ErrnoError, InMemory, configure, configureSingle, fs, resolveMountConfig } from '../../dist/index.js';
|
|
7
8
|
|
|
@@ -49,12 +50,13 @@ timeoutChannel.port1.unref();
|
|
|
49
50
|
// Test configuration
|
|
50
51
|
|
|
51
52
|
const configPort = new Worker(import.meta.dirname + '/config.worker.js');
|
|
53
|
+
await waitOnline(configPort);
|
|
52
54
|
|
|
53
55
|
await suite('Remote FS with resolveRemoteMount', () => {
|
|
54
56
|
const content = 'FS is in a port';
|
|
55
57
|
|
|
56
58
|
test('Configuration', async () => {
|
|
57
|
-
await configureSingle({ backend: Port, port: configPort, timeout:
|
|
59
|
+
await configureSingle({ backend: Port, port: configPort, timeout: 100 });
|
|
58
60
|
});
|
|
59
61
|
|
|
60
62
|
test('Write', async () => {
|
|
@@ -62,11 +64,11 @@ await suite('Remote FS with resolveRemoteMount', () => {
|
|
|
62
64
|
});
|
|
63
65
|
|
|
64
66
|
test('Read', async () => {
|
|
65
|
-
assert(
|
|
67
|
+
assert.equal(await fs.promises.readFile('/test', 'utf8'), content);
|
|
66
68
|
});
|
|
67
69
|
});
|
|
68
70
|
|
|
69
|
-
await configPort
|
|
71
|
+
await configPort.terminate();
|
|
70
72
|
configPort.unref();
|
|
71
73
|
|
|
72
74
|
// Test using a message channel
|
|
@@ -79,7 +81,7 @@ await suite('FS with MessageChannel', () => {
|
|
|
79
81
|
test('configuration', async () => {
|
|
80
82
|
tmpfs = await resolveMountConfig({ backend: InMemory, name: 'tmp' });
|
|
81
83
|
attachFS(channel.port2, tmpfs);
|
|
82
|
-
await configureSingle({ backend: Port, port: channel.port1, disableAsyncCache: true, timeout:
|
|
84
|
+
await configureSingle({ backend: Port, port: channel.port1, disableAsyncCache: true, timeout: 100 });
|
|
83
85
|
});
|
|
84
86
|
|
|
85
87
|
test('write', async () => {
|
|
@@ -88,12 +90,12 @@ await suite('FS with MessageChannel', () => {
|
|
|
88
90
|
|
|
89
91
|
test('remote content', () => {
|
|
90
92
|
fs.mount('/tmp', tmpfs);
|
|
91
|
-
assert(fs.readFileSync('/tmp/test', 'utf8')
|
|
93
|
+
assert.equal(fs.readFileSync('/tmp/test', 'utf8'), content);
|
|
92
94
|
fs.umount('/tmp');
|
|
93
95
|
});
|
|
94
96
|
|
|
95
97
|
test('read', async () => {
|
|
96
|
-
assert(
|
|
98
|
+
assert.equal(await fs.promises.readFile('/test', 'utf8'), content);
|
|
97
99
|
});
|
|
98
100
|
|
|
99
101
|
test('readFileSync should throw', () => {
|
|
@@ -101,6 +103,8 @@ await suite('FS with MessageChannel', () => {
|
|
|
101
103
|
});
|
|
102
104
|
});
|
|
103
105
|
|
|
106
|
+
channel.port1.close();
|
|
107
|
+
channel.port2.close();
|
|
104
108
|
channel.port1.unref();
|
|
105
109
|
channel.port2.unref();
|
|
106
110
|
|
|
@@ -112,7 +116,7 @@ await suite('Remote FS', () => {
|
|
|
112
116
|
const content = 'FS is in a port';
|
|
113
117
|
|
|
114
118
|
test('Configuration', async () => {
|
|
115
|
-
await configureSingle({ backend: Port, port: remotePort, timeout:
|
|
119
|
+
await configureSingle({ backend: Port, port: remotePort, timeout: 100 });
|
|
116
120
|
});
|
|
117
121
|
|
|
118
122
|
test('Write', async () => {
|
|
@@ -122,8 +126,6 @@ await suite('Remote FS', () => {
|
|
|
122
126
|
test('Read', async () => {
|
|
123
127
|
assert.equal(await fs.promises.readFile('/test', 'utf8'), content);
|
|
124
128
|
});
|
|
125
|
-
|
|
126
|
-
test('Cleanup', async () => {});
|
|
127
129
|
});
|
|
128
130
|
|
|
129
131
|
await remotePort.terminate();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { suite, test } from 'node:test';
|
|
3
3
|
import { basename, dirname, extname, join, normalize, resolve } from '../../dist/vfs/path.js';
|
|
4
|
+
import * as fs from '../../dist/vfs/index.js';
|
|
4
5
|
|
|
5
6
|
suite('Path emulation', () => {
|
|
6
7
|
test('resolve', () => {
|
|
@@ -31,4 +32,15 @@ suite('Path emulation', () => {
|
|
|
31
32
|
assert.equal(extname('/path/to/file.txt'), '.txt');
|
|
32
33
|
assert.equal(extname('/path/to/file'), '');
|
|
33
34
|
});
|
|
35
|
+
|
|
36
|
+
test('file:// URL (string)', () => {
|
|
37
|
+
fs.writeFileSync('/example.txt', 'Yay');
|
|
38
|
+
assert.equal(fs.readFileSync('file:///example.txt', 'utf-8'), 'Yay');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('file:// URL (URL)', () => {
|
|
42
|
+
fs.writeFileSync('/example.txt', 'Yay');
|
|
43
|
+
const url = new URL('file:///example.txt');
|
|
44
|
+
assert.equal(fs.readFileSync(url, 'utf-8'), 'Yay');
|
|
45
|
+
});
|
|
34
46
|
});
|
package/tests/fs/links.test.ts
CHANGED
|
@@ -27,6 +27,31 @@ suite('Links', () => {
|
|
|
27
27
|
assert.equal(await fs.promises.readFile(target, 'utf-8'), await fs.promises.readFile(symlink, 'utf-8'));
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
test('nested symlinks', async () => {
|
|
31
|
+
// Create the real directory structure
|
|
32
|
+
const realDir = '/real-dir';
|
|
33
|
+
const realFile = '/real-dir/realfile.txt';
|
|
34
|
+
const fileContent = 'hello world';
|
|
35
|
+
await fs.promises.mkdir(realDir);
|
|
36
|
+
await fs.promises.writeFile(realFile, fileContent);
|
|
37
|
+
// Create first symlink (symlink-dir -> real-dir)
|
|
38
|
+
const symlinkDir = '/symlink-dir';
|
|
39
|
+
await fs.promises.symlink(realDir, symlinkDir);
|
|
40
|
+
const symfile = 'symfile.txt';
|
|
41
|
+
const symlinkFile = join(realDir, symfile);
|
|
42
|
+
// Create second symlink (symlink-dir -> real-dir)
|
|
43
|
+
await fs.promises.symlink(realFile, symlinkFile);
|
|
44
|
+
// Now access file through nested symlinks
|
|
45
|
+
const nestedPath = join(symlinkDir, symfile);
|
|
46
|
+
// Verify realpath resolution
|
|
47
|
+
const resolvedPath = await fs.promises.realpath(nestedPath);
|
|
48
|
+
assert.equal(resolvedPath, realFile);
|
|
49
|
+
// Verify content can be read through nested symlinks
|
|
50
|
+
const content = await fs.promises.readFile(nestedPath, 'utf8');
|
|
51
|
+
assert.notEqual(content, '/real-dir/realfile.txt');
|
|
52
|
+
assert.equal(content, fileContent);
|
|
53
|
+
});
|
|
54
|
+
|
|
30
55
|
test('unlink', async () => {
|
|
31
56
|
await fs.promises.unlink(symlink);
|
|
32
57
|
assert(!(await fs.promises.exists(symlink)));
|