@zenfs/core
Advanced tools
Comparing version 1.9.4 to 1.9.5
@@ -13,3 +13,2 @@ import { StoreFS } from './store/fs.js'; | ||
sync(): Promise<void>; | ||
clearSync(): void; | ||
transaction(): SyncMapTransaction; | ||
@@ -16,0 +15,0 @@ } |
@@ -15,5 +15,2 @@ import { StoreFS } from './store/fs.js'; | ||
async sync() { } | ||
clearSync() { | ||
this.clear(); | ||
} | ||
transaction() { | ||
@@ -20,0 +17,0 @@ return new SyncMapTransaction(this); |
@@ -148,5 +148,5 @@ import { pick } from 'utilium'; | ||
attachFS(port, fs); | ||
stopAndReplay(fs); | ||
await stopAndReplay(fs); | ||
info('Resolved remote mount: ' + fs.toString()); | ||
return fs; | ||
} |
@@ -13,3 +13,3 @@ import type { TransferListItem } from 'node:worker_threads'; | ||
postMessage(value: unknown, transfer?: TransferListItem[]): void; | ||
on?(event: 'message', listener: (value: unknown) => void): this; | ||
on?(event: 'message' | 'online', listener: (value: unknown) => void): this; | ||
off?(event: 'message', listener: (value: unknown) => void): this; | ||
@@ -74,2 +74,6 @@ addEventListener?(type: 'message', listener: (ev: _MessageEvent) => void): void; | ||
export declare function detach<T extends Message>(port: Port, handler: (message: T) => unknown): void; | ||
export declare function catchMessages<T extends Backend>(port: Port): (fs: FilesystemOf<T>) => void; | ||
export declare function catchMessages<T extends Backend>(port: Port): (fs: FilesystemOf<T>) => Promise<void>; | ||
/** | ||
* @internal | ||
*/ | ||
export declare function waitOnline(port: Port): Promise<void>; |
@@ -67,3 +67,3 @@ import { Errno, ErrnoError } from '../../internal/error.js'; | ||
port['on' in port ? 'on' : 'addEventListener']('message', (message) => { | ||
handler('data' in message ? message.data : message); | ||
handler(typeof message == 'object' && message !== null && 'data' in message ? message.data : message); | ||
}); | ||
@@ -76,3 +76,3 @@ } | ||
port['off' in port ? 'off' : 'removeEventListener']('message', (message) => { | ||
handler('data' in message ? message.data : message); | ||
handler(typeof message == 'object' && message !== null && 'data' in message ? message.data : message); | ||
}); | ||
@@ -84,9 +84,20 @@ } | ||
attach(port, handler); | ||
return function (fs) { | ||
return async function (fs) { | ||
detach(port, handler); | ||
for (const event of events) { | ||
const request = 'data' in event ? event.data : event; | ||
void handleRequest(port, fs, request); | ||
await handleRequest(port, fs, request); | ||
} | ||
}; | ||
} | ||
/** | ||
* @internal | ||
*/ | ||
export async function waitOnline(port) { | ||
if (!('on' in port)) | ||
return; // Only need to wait in Node.js | ||
const online = Promise.withResolvers(); | ||
setTimeout(online.reject, 500); | ||
port.on('online', online.resolve); | ||
await online.promise; | ||
} |
import type * as fs from 'node:fs'; | ||
import type { ClassLike, OptionalTuple } from 'utilium'; | ||
import { ErrnoError } from './internal/error.js'; | ||
import type { AbsolutePath } from './vfs/path.js'; | ||
declare global { | ||
@@ -57,3 +56,3 @@ function atob(data: string): string; | ||
*/ | ||
export declare function normalizePath(p: fs.PathLike): AbsolutePath; | ||
export declare function normalizePath(p: fs.PathLike, noResolve?: boolean): string; | ||
/** | ||
@@ -60,0 +59,0 @@ * Normalizes options |
@@ -104,4 +104,11 @@ import { Errno, ErrnoError } from './internal/error.js'; | ||
*/ | ||
export function normalizePath(p) { | ||
export function normalizePath(p, noResolve = false) { | ||
if (p instanceof URL) { | ||
if (p.protocol != 'file:') | ||
throw new ErrnoError(Errno.EINVAL, 'URLs must use the file: protocol'); | ||
p = p.pathname; | ||
} | ||
p = p.toString(); | ||
if (p.startsWith('file://')) | ||
p = p.slice('file://'.length); | ||
if (p.includes('\x00')) { | ||
@@ -113,3 +120,4 @@ throw new ErrnoError(Errno.EINVAL, 'Path can not contain null character'); | ||
} | ||
return resolve(p.replaceAll(/[/\\]+/g, '/')); | ||
p = p.replaceAll(/[/\\]+/g, '/'); | ||
return noResolve ? p : resolve(p); | ||
} | ||
@@ -116,0 +124,0 @@ /** |
@@ -92,5 +92,4 @@ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) { | ||
const numMode = normalizeMode(mode, -1); | ||
if (numMode < 0) { | ||
if (numMode < 0) | ||
throw new ErrnoError(Errno.EINVAL, 'Invalid mode.'); | ||
} | ||
await this.file.chmod(numMode); | ||
@@ -491,6 +490,6 @@ this._emitChange(); | ||
const mode = normalizeMode(opt.mode, 0o644), flag = parseFlag(opt.flag); | ||
const { fullPath: realpath, fs, path: resolved, stats } = await _resolve($, path.toString(), opt.preserveSymlinks); | ||
const { fullPath, fs, path: resolved, stats } = await _resolve($, path.toString(), opt.preserveSymlinks); | ||
if (!stats) { | ||
if ((!isWriteable(flag) && !isAppendable(flag)) || flag == 'r+') { | ||
throw ErrnoError.With('ENOENT', realpath, '_open'); | ||
throw ErrnoError.With('ENOENT', fullPath, '_open'); | ||
} | ||
@@ -500,6 +499,6 @@ // Create the file | ||
if (config.checkAccess && !parentStats.hasAccess(constants.W_OK, $)) { | ||
throw ErrnoError.With('EACCES', dirname(realpath), '_open'); | ||
throw ErrnoError.With('EACCES', dirname(fullPath), '_open'); | ||
} | ||
if (!parentStats.isDirectory()) { | ||
throw ErrnoError.With('ENOTDIR', dirname(realpath), '_open'); | ||
throw ErrnoError.With('ENOTDIR', dirname(fullPath), '_open'); | ||
} | ||
@@ -512,6 +511,6 @@ const { euid: uid, egid: gid } = (_a = $ === null || $ === void 0 ? void 0 : $.credentials) !== null && _a !== void 0 ? _a : credentials; | ||
if (config.checkAccess && !stats.hasAccess(flagToMode(flag), $)) { | ||
throw ErrnoError.With('EACCES', realpath, '_open'); | ||
throw ErrnoError.With('EACCES', fullPath, '_open'); | ||
} | ||
if (isExclusive(flag)) { | ||
throw ErrnoError.With('EEXIST', realpath, '_open'); | ||
throw ErrnoError.With('EEXIST', fullPath, '_open'); | ||
} | ||
@@ -775,7 +774,7 @@ const handle = new FileHandle(await fs.openFile(resolved, flag), $); | ||
} | ||
if (await exists.call(this, path)) { | ||
throw ErrnoError.With('EEXIST', path.toString(), 'symlink'); | ||
} | ||
path = normalizePath(path); | ||
if (await exists.call(this, path)) | ||
throw ErrnoError.With('EEXIST', path, 'symlink'); | ||
const handle = __addDisposableResource(env_5, await _open(this, path, { flag: 'w+', mode: 0o644, preserveSymlinks: true }), true); | ||
await handle.writeFile(target.toString()); | ||
await handle.writeFile(normalizePath(target, true)); | ||
await handle.file.chmod(constants.S_IFLNK); | ||
@@ -973,4 +972,3 @@ } | ||
const target = resolve(realDir, (await readlink.call($, maybePath)).toString()); | ||
const real = await realpath.call($, target); | ||
return { ...resolved, fullPath: real, stats }; | ||
return await _resolve($, target); | ||
} | ||
@@ -977,0 +975,0 @@ catch (e) { |
@@ -238,7 +238,8 @@ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) { | ||
} | ||
function _readFileSync(fname, flag, preserveSymlinks) { | ||
function _readFileSync(path, flag, preserveSymlinks) { | ||
const env_2 = { stack: [], error: void 0, hasError: false }; | ||
try { | ||
path = normalizePath(path); | ||
// Get file. | ||
const file = __addDisposableResource(env_2, _openSync.call(this, fname, { flag, mode: 0o644, preserveSymlinks }), false); | ||
const file = __addDisposableResource(env_2, _openSync.call(this, path, { flag, mode: 0o644, preserveSymlinks }), false); | ||
const stat = file.statSync(); | ||
@@ -264,3 +265,3 @@ // Allocate buffer. | ||
} | ||
const data = Buffer.from(_readFileSync.call(this, typeof path == 'number' ? fd2file(path).path : path.toString(), options.flag, false)); | ||
const data = Buffer.from(_readFileSync.call(this, typeof path == 'number' ? fd2file(path).path : path, options.flag, false)); | ||
return options.encoding ? data.toString(options.encoding) : data; | ||
@@ -569,3 +570,3 @@ } | ||
} | ||
writeFileSync.call(this, path, target.toString()); | ||
writeFileSync.call(this, path, normalizePath(target, true)); | ||
const file = _openSync.call(this, path, { flag: 'r+', mode: 0o644, preserveSymlinks: true }); | ||
@@ -576,3 +577,3 @@ file.chmodSync(constants.S_IFLNK); | ||
export function readlinkSync(path, options) { | ||
const value = Buffer.from(_readFileSync.call(this, path.toString(), 'r', true)); | ||
const value = Buffer.from(_readFileSync.call(this, path, 'r', true)); | ||
const encoding = typeof options == 'object' ? options === null || options === void 0 ? void 0 : options.encoding : options; | ||
@@ -664,4 +665,3 @@ if (encoding == 'buffer') { | ||
const target = resolve(realDir, readlinkSync.call($, maybePath).toString()); | ||
const real = realpathSync.call($, target); | ||
return { ...resolved, fullPath: real, stats }; | ||
return _resolveSync($, target); | ||
} | ||
@@ -668,0 +668,0 @@ catch (e) { |
{ | ||
"name": "@zenfs/core", | ||
"version": "1.9.4", | ||
"version": "1.9.5", | ||
"description": "A filesystem, anywhere", | ||
@@ -5,0 +5,0 @@ "funding": { |
@@ -54,3 +54,3 @@ #!/usr/bin/env node | ||
-q, --quiet Don't output normal messages | ||
-l, --logs <level> Change the default log level for test output. Level can be a number or string | ||
-l, --logs <level> Change the default log level for test output. Level can be a number or string | ||
-N, --file-names Use full file paths for tests from setup files instead of the base name | ||
@@ -170,5 +170,8 @@ -C, --ci Continuous integration (CI) mode. This interacts with the Github | ||
try { | ||
execSync(`tsx ${options.inspect ? 'inspect' : ''} --test --experimental-test-coverage 'tests/*.test.ts' 'tests/**/!(fs)/*.test.ts'`, { | ||
stdio: ['ignore', options.verbose ? 'inherit' : 'ignore', 'inherit'], | ||
}); | ||
execSync( | ||
`tsx ${options.inspect ? 'inspect' : ''} ${options.force ? '--test-force-exit' : ''} --test --experimental-test-coverage 'tests/*.test.ts' 'tests/**/!(fs)/*.test.ts'`, | ||
{ | ||
stdio: ['ignore', options.verbose ? 'inherit' : 'ignore', 'inherit'], | ||
} | ||
); | ||
await pass(); | ||
@@ -175,0 +178,0 @@ } catch { |
@@ -5,2 +5,3 @@ import assert from 'node:assert/strict'; | ||
import { Port, attachFS } from '../../dist/backends/port/fs.js'; | ||
import { waitOnline } from '../../dist/backends/port/rpc.js'; | ||
import type { InMemoryStore, StoreFS } from '../../dist/index.js'; | ||
@@ -53,2 +54,3 @@ import { ErrnoError, InMemory, configure, configureSingle, fs, resolveMountConfig } from '../../dist/index.js'; | ||
const configPort = new Worker(import.meta.dirname + '/config.worker.js'); | ||
await waitOnline(configPort); | ||
@@ -59,3 +61,3 @@ await suite('Remote FS with resolveRemoteMount', () => { | ||
test('Configuration', async () => { | ||
await configureSingle({ backend: Port, port: configPort, timeout: 500 }); | ||
await configureSingle({ backend: Port, port: configPort, timeout: 100 }); | ||
}); | ||
@@ -68,7 +70,7 @@ | ||
test('Read', async () => { | ||
assert((await fs.promises.readFile('/test', 'utf8')) === content); | ||
assert.equal(await fs.promises.readFile('/test', 'utf8'), content); | ||
}); | ||
}); | ||
await configPort?.terminate(); | ||
await configPort.terminate(); | ||
configPort.unref(); | ||
@@ -86,3 +88,3 @@ | ||
attachFS(channel.port2, tmpfs); | ||
await configureSingle({ backend: Port, port: channel.port1, disableAsyncCache: true, timeout: 250 }); | ||
await configureSingle({ backend: Port, port: channel.port1, disableAsyncCache: true, timeout: 100 }); | ||
}); | ||
@@ -96,3 +98,3 @@ | ||
fs.mount('/tmp', tmpfs); | ||
assert(fs.readFileSync('/tmp/test', 'utf8') == content); | ||
assert.equal(fs.readFileSync('/tmp/test', 'utf8'), content); | ||
fs.umount('/tmp'); | ||
@@ -102,3 +104,3 @@ }); | ||
test('read', async () => { | ||
assert((await fs.promises.readFile('/test', 'utf8')) === content); | ||
assert.equal(await fs.promises.readFile('/test', 'utf8'), content); | ||
}); | ||
@@ -111,2 +113,4 @@ | ||
channel.port1.close(); | ||
channel.port2.close(); | ||
channel.port1.unref(); | ||
@@ -123,3 +127,3 @@ channel.port2.unref(); | ||
test('Configuration', async () => { | ||
await configureSingle({ backend: Port, port: remotePort, timeout: 500 }); | ||
await configureSingle({ backend: Port, port: remotePort, timeout: 100 }); | ||
}); | ||
@@ -134,4 +138,2 @@ | ||
}); | ||
test('Cleanup', async () => {}); | ||
}); | ||
@@ -138,0 +140,0 @@ |
import assert from 'node:assert/strict'; | ||
import { suite, test } from 'node:test'; | ||
import { basename, dirname, extname, join, normalize, resolve } from '../../dist/vfs/path.js'; | ||
import * as fs from '../../dist/vfs/index.js'; | ||
@@ -34,2 +35,13 @@ suite('Path emulation', () => { | ||
}); | ||
test('file:// URL (string)', () => { | ||
fs.writeFileSync('/example.txt', 'Yay'); | ||
assert.equal(fs.readFileSync('file:///example.txt', 'utf-8'), 'Yay'); | ||
}); | ||
test('file:// URL (URL)', () => { | ||
fs.writeFileSync('/example.txt', 'Yay'); | ||
const url = new URL('file:///example.txt'); | ||
assert.equal(fs.readFileSync(url, 'utf-8'), 'Yay'); | ||
}); | ||
}); |
@@ -30,2 +30,27 @@ import assert from 'node:assert/strict'; | ||
test('nested symlinks', async () => { | ||
// Create the real directory structure | ||
const realDir = '/real-dir'; | ||
const realFile = '/real-dir/realfile.txt'; | ||
const fileContent = 'hello world'; | ||
await fs.promises.mkdir(realDir); | ||
await fs.promises.writeFile(realFile, fileContent); | ||
// Create first symlink (symlink-dir -> real-dir) | ||
const symlinkDir = '/symlink-dir'; | ||
await fs.promises.symlink(realDir, symlinkDir); | ||
const symfile = 'symfile.txt'; | ||
const symlinkFile = join(realDir, symfile); | ||
// Create second symlink (symlink-dir -> real-dir) | ||
await fs.promises.symlink(realFile, symlinkFile); | ||
// Now access file through nested symlinks | ||
const nestedPath = join(symlinkDir, symfile); | ||
// Verify realpath resolution | ||
const resolvedPath = await fs.promises.realpath(nestedPath); | ||
assert.equal(resolvedPath, realFile); | ||
// Verify content can be read through nested symlinks | ||
const content = await fs.promises.readFile(nestedPath, 'utf8'); | ||
assert.notEqual(content, '/real-dir/realfile.txt'); | ||
assert.equal(content, fileContent); | ||
}); | ||
test('unlink', async () => { | ||
@@ -32,0 +57,0 @@ await fs.promises.unlink(symlink); |
694458
17612