Skip to content

[Windows] 文件浏览器显示了系统文件与 junction 目录(NTUSER.DAT、「开始」菜单、Application Data 等) #390

Description

@Hsiung233

环境: Windows 11 · pi-web 0.8.6 · pi 0.83.0

现象: 文件浏览器打开用户主目录(C:\Users\<用户名>)时,会显示 Windows 的资源管理器默认隐藏的系统文件和目录:

  • 系统文件:NTUSER.DATntuser.dat.LOG1/2NTUSER.DAT{...}.TM.blf.TMContainer...regtrans-msntuser.inidesktop.ini
  • junction 目录:「开始」菜单、Application DataLocal SettingsNetHoodPrintHoodRecentSendToTemplatesCookies

这些条目都带 Hidden + System 属性(junction 还有 ReparsePoint),Windows 资源管理器默认不显示;在文件浏览器里大量出现,容易被误认为是隐私/安全问题。

原因分析(对照源码):

  1. lib/directory-browser.tslistDirectoriesfs.readdir(dir, { withFileTypes: true })——Node.js 的 readdir 不过滤 Windows 的 FILE_ATTRIBUTE_HIDDEN / FILE_ATTRIBUTE_SYSTEM,原样返回所有条目。资源管理器隐藏它们靠的是 shell 层的属性过滤,Node 没有这个约定。
  2. junction 被当作符号链接处理并特意收录(isSymbolicLink → realpath → stat → isDirectory),所以「开始」菜单/Application Data 这类指向真实目录的 junction 会显示(指向不存在目标的 My Documents 会被过滤掉,可作旁证)。
  3. app/api/file-index/route.tslistWithWalk BFS 遍历只过滤了 IGNORED_NAMES(node_modules 等)和后缀黑名单,同样没有 Windows 属性过滤;NTUSER.DATntuser.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.tslistDirectories 增加过滤

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.tslistWithWalk 增加轻量名字过滤

深遍历(最多 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 仓库走 listWithGitgit ls-files + .gitignore),不受影响。
  • 若不想 spawn PowerShell,可用 winattr/fswin 原生模块替换 helper,语义等价;但不要用 attrib 命令(对 junction 目录的属性报告不可靠)。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions