- fs-tree: requireAbsolute 改用 path.isAbsolute(win32 UNC 放行、C:foo 仍拒); 目录列表仅对 symlink 行 stat 一次,暴露 isSymlink/broken,目录软链接可展开 - html-route: UNC 路径经 '//' 标记编码往返;标记仅在 Windows 风格会话启用, POSIX '//' 绝对路径不再被误判(平台守卫) - client: isAbsolutePath/isWindowsStylePath 镜像 + resolveSidebarPath 识别 UNC, htmlUrl 以会话 cwd 形态传递平台信号;FsEntry 增加 isSymlink/broken - explorer: 软链接行链接徽标、悬空链接标红 + tooltip(issue #65) - 测试: tests/platform.ts 守卫助手;win32 专属用例改为 skipIf 可见跳过, 补 POSIX 反向分支与 UNC 矩阵;新增 fs-tree-symlink.spec
This commit is contained in:
+32
-10
@@ -2,11 +2,13 @@
|
||||
* Single-level directory listing for the sidebar explorer. Streams the level
|
||||
* with opendir, sorts directories first then names (case-insensitive), and
|
||||
* marks POSIX-hidden entries (dot-prefixed) for dimmed display. Symlinks are
|
||||
* reported as files without probing their target — the explorer shows what
|
||||
* dirent says, keeping the read cheap for arbitrarily large levels.
|
||||
* stat'ed once to expose their target kind — a symlink to a directory
|
||||
* expands like a directory — and dangling links are flagged broken. The
|
||||
* probe runs only for entries that are actually symlinks, so levels without
|
||||
* links stay as cheap as before.
|
||||
*/
|
||||
import { opendir } from 'node:fs/promises'
|
||||
import { basename, dirname, join, resolve } from 'node:path'
|
||||
import { opendir, stat } from 'node:fs/promises'
|
||||
import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
|
||||
import { SidebarError } from './wire.ts'
|
||||
|
||||
/** One explorer row. */
|
||||
@@ -15,6 +17,10 @@ export interface SidebarFsEntry {
|
||||
path: string
|
||||
isDir: boolean
|
||||
hidden: boolean
|
||||
/** Whether the row is a symlink; `isDir` then describes the link's target. */
|
||||
isSymlink: boolean
|
||||
/** For symlinks: the target is missing or unreadable (stat failed). */
|
||||
broken: boolean
|
||||
}
|
||||
|
||||
/** One listed level. */
|
||||
@@ -52,12 +58,22 @@ export async function listDirectory(path: string, maxEntries = 1000): Promise<Si
|
||||
overflow += 1
|
||||
continue
|
||||
}
|
||||
// Platform join: on Windows the level path uses '\' — a hardcoded '/'
|
||||
// would leak mixed separators into every row's path.
|
||||
const fullPath = join(path, dirent.name)
|
||||
const isSymlink = dirent.isSymbolicLink()
|
||||
// Probe the link's target once so a symlinked directory renders as an
|
||||
// expandable directory row. stat follows the chain; any failure
|
||||
// (missing target, ELOOP, permission) leaves the row as a broken
|
||||
// file-shaped link the editor will refuse to read. Non-symlink entries
|
||||
// skip the syscall entirely — dirent already classified them.
|
||||
const info = isSymlink ? await stat(fullPath).catch(() => undefined) : undefined
|
||||
rows.push({
|
||||
name: dirent.name,
|
||||
// Platform join: on Windows the level path uses '\' — a hardcoded '/'
|
||||
// would leak mixed separators into every row's path.
|
||||
path: join(path, dirent.name),
|
||||
isDir: dirent.isDirectory(),
|
||||
path: fullPath,
|
||||
isDir: info !== undefined ? info.isDirectory() : dirent.isDirectory(),
|
||||
isSymlink,
|
||||
broken: isSymlink && info === undefined,
|
||||
hidden: dirent.name.startsWith('.'),
|
||||
})
|
||||
}
|
||||
@@ -80,9 +96,15 @@ export function parentOf(path: string): string | undefined {
|
||||
return parent === path ? undefined : parent
|
||||
}
|
||||
|
||||
/** Normalize a caller-supplied path to an absolute, resolved path or throw fs-error. */
|
||||
/**
|
||||
* Normalize a caller-supplied path to an absolute, resolved path or throw
|
||||
* fs-error. `path.isAbsolute()` is the OS's own notion of absolute: POSIX
|
||||
* roots (`/...`), Windows drive letters (`C:\...`) and — on win32 — UNC
|
||||
* network shares (`\\server\share\...`); drive-relative forms (`C:foo`)
|
||||
* stay rejected.
|
||||
*/
|
||||
export function requireAbsolute(path: string): string {
|
||||
if (!path.startsWith('/') && !/^[A-Za-z]:[\\/]/.test(path)) {
|
||||
if (!isAbsolute(path)) {
|
||||
throw new SidebarError('fs-error', `"${path}" is not an absolute path`, 400)
|
||||
}
|
||||
return resolve(path)
|
||||
|
||||
Reference in New Issue
Block a user