fix(fs): translate listDir metadata failures

This commit is contained in:
Yichen Jiang
2026-07-03 15:12:40 +08:00
parent 803ed4bd95
commit 6fc2cee837
11 changed files with 144 additions and 33 deletions

View File

@@ -55,11 +55,9 @@ function errorMessage(error: unknown): string {
}
/* v8 ignore stop */
/* v8 ignore start -- requires permission/kernel failures from readdir after a successful directory stat. */
function isPermissionError(error: unknown): boolean {
return error instanceof Error && 'code' in error && (error.code === 'EACCES' || error.code === 'EPERM')
}
/* v8 ignore stop */
function throwIfAborted(signal: AbortSignal | undefined, verb: string): void {
if (signal?.aborted) throw new FsError(`${verb} aborted`, 'FS_ABORTED')
@@ -189,13 +187,14 @@ export async function probe(absolutePath: string): Promise<PathInfo | null> {
// --- Directory listing ---
/* v8 ignore start -- requires permission/kernel failures from readdir after a successful directory stat. */
function listingIoError(displayPath: string, error: unknown): FsError {
/* v8 ignore next -- defensive pass-through for races where a child resolver has already produced a structured FsError. */
if (error instanceof FsError) return error
/* v8 ignore next -- requires the listed target/parent to disappear between successful preflight and listing/child resolution. */
if (isENOENT(error) || isENOTDIR(error)) return new FsError(`cannot list "${displayPath}": not found`, 'FS_NOT_FOUND', { cause: error })
if (isPermissionError(error)) return new FsError(`cannot list "${displayPath}": permission denied`, 'FS_PERMISSION_DENIED', { cause: error })
return new FsError(`cannot list "${displayPath}": ${errorMessage(error)}`, 'FS_IO_ERROR', { cause: error })
}
/* v8 ignore stop */
/**
* List direct children of a directory in stable name order. Each child includes
@@ -204,7 +203,12 @@ function listingIoError(displayPath: string, error: unknown): FsError {
*/
export async function listDirectory(target: LocalTarget, signal?: AbortSignal): Promise<LocalDirEntry[]> {
throwIfAborted(signal, 'list')
const info = await probe(target.targetKey)
let info: PathInfo | null
try {
info = await probe(target.targetKey)
} catch (error: unknown) {
throw listingIoError(target.displayPath, error)
}
if (!info) throw new FsError(`cannot list "${target.displayPath}": not found`, 'FS_NOT_FOUND')
if (info.type !== 'directory') throw new FsError(`cannot list "${target.displayPath}": not a directory`, 'FS_NOT_DIRECTORY')
@@ -217,19 +221,25 @@ export async function listDirectory(target: LocalTarget, signal?: AbortSignal):
}
throwIfAborted(signal, 'list')
return await Promise.all(entries
.sort((left, right) => left.name.localeCompare(right.name))
.map(async (entry): Promise<LocalDirEntry> => {
const result: LocalDirEntry[] = []
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
throwIfAborted(signal, 'list')
try {
const childTarget = await resolveLocalTarget(target.displayPath, entry.name)
const childInfo = await probe(childTarget.targetKey)
return {
result.push({
name: entry.name,
type: childInfo?.type ?? 'other',
target: childTarget,
...(childInfo ? { version: childInfo.version } : {}),
...(childInfo?.type === 'file' ? { size: childInfo.size } : {}),
}
}))
})
} catch (error: unknown) {
throw listingIoError(join(target.displayPath, entry.name), error)
}
throwIfAborted(signal, 'list')
}
return result
}
// --- Reading ---

View File

@@ -125,7 +125,7 @@ export class LocalFileSystem extends FileSystem {
return entries.map(entry => ({
name: entry.name,
type: entry.type,
target: { inputPath: entry.target.displayPath, targetKey: entry.target.targetKey, displayPath: entry.target.displayPath },
target: { inputPath: entry.name, targetKey: entry.target.targetKey, displayPath: entry.target.displayPath },
...(entry.version !== undefined ? { version: entry.version } : {}),
...(entry.size !== undefined ? { size: entry.size } : {}),
}))