环境: Windows 11 · pi-web 0.8.6 · pi 0.83.0
现象: 文件浏览器打开用户主目录(C:\Users\<用户名>)时,会显示 Windows 的资源管理器默认隐藏的系统文件和目录:
- 系统文件:
NTUSER.DAT、ntuser.dat.LOG1/2、NTUSER.DAT{...}.TM.blf、.TMContainer...regtrans-ms、ntuser.ini、desktop.ini 等
- junction 目录:「开始」菜单、
Application Data、Local Settings、NetHood、PrintHood、Recent、SendTo、Templates、Cookies 等
这些条目都带 Hidden + System 属性(junction 还有 ReparsePoint),Windows 资源管理器默认不显示;在文件浏览器里大量出现,容易被误认为是隐私/安全问题。
原因分析(对照源码):
lib/directory-browser.ts 的 listDirectories 用 fs.readdir(dir, { withFileTypes: true })——Node.js 的 readdir 不过滤 Windows 的 FILE_ATTRIBUTE_HIDDEN / FILE_ATTRIBUTE_SYSTEM,原样返回所有条目。资源管理器隐藏它们靠的是 shell 层的属性过滤,Node 没有这个约定。
- junction 被当作符号链接处理并特意收录(
isSymbolicLink → realpath → stat → isDirectory),所以「开始」菜单/Application Data 这类指向真实目录的 junction 会显示(指向不存在目标的 My Documents 会被过滤掉,可作旁证)。
app/api/file-index/route.ts 的 listWithWalk BFS 遍历只过滤了 IGNORED_NAMES(node_modules 等)和后缀黑名单,同样没有 Windows 属性过滤;NTUSER.DAT、ntuser.ini 等因此进入文件列表。
影响: 只读展示问题,不影响功能,但对 Windows 用户体验影响较大。
补丁建议(已对照源码 + 本机实测验证)
涉及文件:
lib/directory-browser.ts(目录浏览 listDirectories)
app/api/file-index/route.ts(文件索引 listWithWalk)
① 新增 lib/windows-attrs.ts(Windows 专用小助手)
⚠️ 实测结论:attrib 对 junction/目录的 H/S 属性报告不可靠(同一批 junction 里 Cookies 报 SH、Application Data 不报);PowerShell 属性枚举稳定,本机实测能命中全部 10 个 junction 目录 + 7 个系统文件(NTUSER.DAT* 系列)。因此用 PowerShell 实现。
import { execFile } from "child_process";
import { promisify } from "util";
const execFileAsync = promisify(execFile);
/** Windows: 返回目录中带 Hidden/System 属性的条目名集合;非 Windows 返回 null。 */
export async function listWindowsHiddenOrSystemNames(
directory: string,
): Promise<Set<string> | null> {
if (process.platform !== "win32") return null;
const script = [
"Get-ChildItem -Force -LiteralPath $args[0] |",
"Where-Object { $_.Attributes -band [IO.FileAttributes]::Hidden -or",
" $_.Attributes -band [IO.FileAttributes]::System } |",
"Select-Object -ExpandProperty Name",
].join(" ");
try {
const { stdout } = await execFileAsync(
"powershell",
["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script, directory],
{ timeout: 10_000, windowsHide: true, maxBuffer: 1024 * 1024 },
);
return new Set(stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean));
} catch {
return null; // PowerShell 不可用时降级为不过滤
}
}
② lib/directory-browser.ts 的 listDirectories 增加过滤
import { listWindowsHiddenOrSystemNames } from "./windows-attrs";
export async function listDirectories(directory: string): Promise<BrowsableDirectory[]> {
const entries = await readdir(directory, { withFileTypes: true });
const hidden = await listWindowsHiddenOrSystemNames(directory); // ← 新增
// 忽略损坏、不可访问或不指向目录的符号链接。
const candidates = await Promise.all(entries.map(async (entry) => {
if (hidden?.has(entry.name)) return null; // ← 新增:过滤 H/S 条目(含 junction 目录)
if (entry.isDirectory()) {
return { name: entry.name, path: path.join(directory, entry.name) };
}
if (!entry.isSymbolicLink()) return null;
try {
const entryPath = path.join(directory, entry.name);
const realEntryPath = await realpath(entryPath);
const entryStat = await stat(realEntryPath);
if (!entryStat.isDirectory()) return null;
return { name: entry.name, path: entryPath };
} catch {
return null;
}
}));
return candidates
.filter((entry): entry is BrowsableDirectory => entry !== null)
.sort((left, right) => left.name.localeCompare(right.name));
}
③ app/api/file-index/route.ts 的 listWithWalk 增加轻量名字过滤
深遍历(最多 8 层 BFS)不宜对每个目录 spawn PowerShell,用名字模式覆盖已知系统文件:
// 与现有 IGNORED_NAMES 并列:
const WINDOWS_SYSTEM_PATTERNS =
process.platform === "win32"
? [/^ntuser\.dat/i, /^desktop\.ini$/i, /^thumbs\.db$/i, /\.tm\.blf$/i, /\.regtrans-ms$/i,
/^(pagefile|hiberfil|swapfile)\.sys$/i]
: [];
// listWithWalk 循环体内,紧跟现有 ignore 判断之后:
if (WINDOWS_SYSTEM_PATTERNS.some((re) => re.test(d.name))) continue;
设计说明
listDirectories(浏览 API)每次调用只列一个目录,PowerShell 每次仅 spawn 一次(冷启动约 0.5–1.5s,可接受),且能精确覆盖任意 H/S 条目(包括 junction 目录)。
listWithWalk 是深遍历,逐目录 spawn PowerShell 太重,用名字模式过滤已知系统文件,已覆盖本 issue 报告的全部现象。
- git 仓库走
listWithGit(git ls-files + .gitignore),不受影响。
- 若不想 spawn PowerShell,可用
winattr/fswin 原生模块替换 helper,语义等价;但不要用 attrib 命令(对 junction 目录的属性报告不可靠)。
环境: Windows 11 · pi-web 0.8.6 · pi 0.83.0
现象: 文件浏览器打开用户主目录(
C:\Users\<用户名>)时,会显示 Windows 的资源管理器默认隐藏的系统文件和目录:NTUSER.DAT、ntuser.dat.LOG1/2、NTUSER.DAT{...}.TM.blf、.TMContainer...regtrans-ms、ntuser.ini、desktop.ini等Application Data、Local Settings、NetHood、PrintHood、Recent、SendTo、Templates、Cookies等这些条目都带
Hidden + System属性(junction 还有ReparsePoint),Windows 资源管理器默认不显示;在文件浏览器里大量出现,容易被误认为是隐私/安全问题。原因分析(对照源码):
lib/directory-browser.ts的listDirectories用fs.readdir(dir, { withFileTypes: true })——Node.js 的readdir不过滤 Windows 的FILE_ATTRIBUTE_HIDDEN/FILE_ATTRIBUTE_SYSTEM,原样返回所有条目。资源管理器隐藏它们靠的是 shell 层的属性过滤,Node 没有这个约定。isSymbolicLink → realpath → stat → isDirectory),所以「开始」菜单/Application Data 这类指向真实目录的 junction 会显示(指向不存在目标的 My Documents 会被过滤掉,可作旁证)。app/api/file-index/route.ts的listWithWalkBFS 遍历只过滤了IGNORED_NAMES(node_modules 等)和后缀黑名单,同样没有 Windows 属性过滤;NTUSER.DAT、ntuser.ini等因此进入文件列表。影响: 只读展示问题,不影响功能,但对 Windows 用户体验影响较大。
补丁建议(已对照源码 + 本机实测验证)
涉及文件:
lib/directory-browser.ts(目录浏览listDirectories)app/api/file-index/route.ts(文件索引listWithWalk)① 新增
lib/windows-attrs.ts(Windows 专用小助手)②
lib/directory-browser.ts的listDirectories增加过滤③
app/api/file-index/route.ts的listWithWalk增加轻量名字过滤深遍历(最多 8 层 BFS)不宜对每个目录 spawn PowerShell,用名字模式覆盖已知系统文件:
设计说明
listDirectories(浏览 API)每次调用只列一个目录,PowerShell 每次仅 spawn 一次(冷启动约 0.5–1.5s,可接受),且能精确覆盖任意 H/S 条目(包括 junction 目录)。listWithWalk是深遍历,逐目录 spawn PowerShell 太重,用名字模式过滤已知系统文件,已覆盖本 issue 报告的全部现象。listWithGit(git ls-files+.gitignore),不受影响。winattr/fswin原生模块替换 helper,语义等价;但不要用attrib命令(对 junction 目录的属性报告不可靠)。