当前位置: 首页 > news >正文

Rust实现Windows本地搜索工具

用 Rust 读取 NTFS MFT/USN Journal本地文件索引器

GitHub 仓库地址:https://github.com/ning696/FilePulse

工具展示:

前言

Windows 上的文件搜索工具有很多,最有代表性的当然是 Everything。它之所以快,很重要的原因是它没有像普通程序那样递归遍历每一个目录,而是利用了 NTFS 文件系统本身维护的底层数据。

我最近写了一个实验型项目 FilePulse,目标不是一开始就完整替代 Everything,而是用 Rust 实现一个更接近系统底层的本地文件索引核心:

  • 读取本地 NTFS 卷信息
  • 通过 MFT 枚举文件记录
  • 根据文件引用关系重建完整路径
  • 使用 USN Journal 增量同步文件变化
  • 将索引快照保存到本地,下次启动时直接恢复

这篇文章只聊 Rust 后端部分,不展开前端 UI。Tauri 和 React 在这个项目里主要负责桌面界面,真正有意思的部分在 Rust 对 Windows NTFS API 的封装和索引逻辑。

为什么不是递归遍历目录

一个最直接的文件搜索 MVP 通常会这样写:

选择目录 -> 递归遍历 -> 收集文件路径 -> 放进 Vec -> 搜索

这种方案简单、跨平台,也容易实现。但它有几个明显问题:

  • 首次扫描慢,尤其是文件数量很多时
  • 每次启动都重新扫描,体验不稳定
  • 文件变化后很难高效增量更新
  • 扫描过程依赖目录树遍历,本质上没有利用文件系统已经维护好的元数据

FilePulse 采用的是另一条路线:

选择 NTFS 卷 -> 打开卷句柄 -> 查询 USN Journal 状态 -> 使用 FSCTL_ENUM_USN_DATA 枚举 MFT/USN 记录 -> 根据 file_ref / parent_ref 重建路径 -> 建立内存索引 -> 保存索引快照 -> 使用 FSCTL_READ_USN_JOURNAL 读取增量变化

也就是说,我不是从目录树一层一层走下去,而是直接读取 NTFS 暴露出来的文件记录,再在应用层把它们整理成可搜索的数据结构。

技术栈

Rust 侧主要使用:

  • windows-sys:调用 Windows API
  • serde:序列化索引结构
  • bincode:保存二进制索引快照
  • tokio:处理后台同步任务
  • anyhow/thiserror:错误处理

项目的 Rust 核心模块大致如下:

src-tauri/src/core/windows_ntfs.rs # Windows NTFS API 封装 src-tauri/src/core/index.rs # 内存索引、路径重建、增量合并 src-tauri/src/core/model.rs # 核心数据结构 src-tauri/src/core/storage.rs # 索引快照保存和加载 src-tauri/src/core/search.rs # 搜索匹配和排序 src-tauri/src/monitor.rs # 后台 USN Journal 监听 src-tauri/src/commands.rs # Tauri 命令入口

第一步:枚举本地 NTFS 卷

要读取 MFT/USN,首先需要知道当前机器上有哪些可用卷。项目里通过 Windows API 枚举逻辑驱动器,然后筛选本地固定磁盘和 NTFS 文件系统。

核心流程是:

GetLogicalDrives -> GetDriveTypeW -> GetVolumeInformationW -> 判断是否为本地固定 NTFS 卷

这样可以排除移动盘、网络盘、光驱,以及非 NTFS 文件系统。

这个限制是有意为之。MFT 和 USN Journal 都是 NTFS 相关能力,项目当前阶段只聚焦 Windows 本地 NTFS 卷,避免为了兼容性过早复杂化。

第二步:打开卷句柄

普通文件可以用路径打开,比如:

C:\Users\example.txt

但如果要读取整个卷的底层信息,需要打开类似这样的卷路径:

\\.\F:

Rust 里通过CreateFileW打开卷句柄:

letpath=format!(r"\\.\{}",volume.trim_end_matches('\\'));

这里有几个关键点:

  • 路径需要转成 UTF-16,并以0结尾
  • 通常需要管理员权限
  • 需要允许读、写、删除共享,避免和系统或其他程序冲突
  • 句柄使用完必须关闭,因此我封装了VolumeHandle,在Drop中调用CloseHandle

这也是 Rust 比较舒服的地方:Windows API 本身是偏 C 风格的,但可以把不安全操作收拢在一个小模块里,再用 RAII 管理资源释放。

第三步:查询 USN Journal

打开卷句柄后,需要先查询当前 USN Journal 的状态。

项目里调用:

FSCTL_QUERY_USN_JOURNAL

拿到的信息里最重要的是:

  • UsnJournalID:当前 Journal 的 ID
  • FirstUsn:当前还保留的最早 USN
  • NextUsn:下一条 USN 位置

这些字段会被保存到索引状态里:

pubstructVolumeIndexState{pubvolume:String,pubjournal_id:u64,pubfirst_usn:i64,pubnext_usn:i64,pubindexed_at:u64,publast_synced_at:u64,pubsource:String,}

后续增量同步时,就可以从上次保存的next_usn开始继续读取变化。

第四步:枚举 MFT/USN 记录

首次建立索引时,项目使用:

FSCTL_ENUM_USN_DATA

它可以枚举卷中的文件记录。每条记录里包含一些非常关键的信息:

  • 文件引用号FileReferenceNumber
  • 父目录引用号ParentFileReferenceNumber
  • 文件名
  • 文件属性
  • 时间戳
  • 当前 USN

这些信息会被转换成项目内部的NtfsRecord

pubstructNtfsRecord{pubfile_ref:u64,pubparent_ref:u64,pubname:String,pubattributes:u32,pubusn:i64,pubtimestamp:i64,pubreason:u32,}

这里最需要小心的是 buffer 解析。Windows API 返回的是一段字节缓冲区,里面连续存放多条USN_RECORD。Rust 侧需要按RecordLength一条一条读取,同时把 UTF-16 文件名转换成String

这部分代码会不可避免地用到unsafe,但原则是让unsafe尽量局部化:只在解析 Windows 原始结构体时使用,解析完成后立刻转成普通 Rust 结构体。

第五步:根据 file_ref / parent_ref 重建完整路径

MFT/USN 记录本身不会直接给出完整路径。它给的是:

当前文件的 file_ref 父目录的 parent_ref 当前文件名 name

也就是说,每条记录更像是一个节点。要得到完整路径,需要根据父子关系往上回溯。

例如:

5 -> . -> F:\ 10 -> src -> parent = 5 11 -> main.rs -> parent = 10

最终重建出:

F:\src\main.rs

项目里使用HashMap<u64, (u64, String)>保存原始关系,然后用递归加 memo 缓存解析路径:

file_ref -> (parent_ref, name)

重建路径时需要处理几个边界情况:

  • 根目录的 parent 可能指向自己
  • 父节点可能缺失
  • 关系异常时要避免递归环
  • 路径拼接要处理卷根目录的反斜杠

这部分是整个索引器里非常核心的一步。因为只有把 NTFS 的底层记录转换成完整路径,后面搜索、打开、定位文件才有意义。

第六步:建立应用层索引

底层的NtfsRecord还不能直接用于搜索。项目会把它转换成更适合应用层使用的FileEntry

pubstructFileEntry{pubid:u64,pubvolume:String,pubfile_ref:u64,pubparent_ref:u64,pubname:String,pubpath:String,pubextension:Option<String>,pubsize:Option<u64>,pubmodified:u64,pubis_dir:bool,publast_usn:i64,pubattributes:u32,}

内存索引里主要维护:

  • entries: Vec<FileEntry>:所有可搜索条目
  • searchable: Vec<String>:预处理后的搜索文本
  • by_file_ref: HashMap<u64, usize>:按文件引用快速定位
  • children_by_parent: HashMap<u64, Vec<u64>>:按父目录找到子节点

搜索逻辑当前比较克制,只做文件名匹配:

  • 大小写不敏感
  • 完全匹配优先
  • 前缀匹配其次
  • 目录排在文件前面
  • 默认最多返回 500 条结果

这个阶段没有急着做复杂查询语法,因为底层索引的正确性更重要。

第七步:使用 USN Journal 增量同步

首次索引之后,如果每次文件变化都重新枚举整个 MFT,成本依然很高。真正让它像文件搜索器的地方,是通过 USN Journal 做增量同步。

同步流程是:

读取上次保存的 VolumeIndexState -> 打开卷 -> 查询当前 USN Journal -> 判断 journal_id 是否变化 -> 判断 first_usn 是否已经超过上次 next_usn -> 从 next_usn 开始读取变化记录 -> 合并到内存索引 -> 更新 next_usn

如果journal_id变了,或者当前first_usn已经大于上次保存的next_usn,说明中间的记录已经无法完整读取。此时不能继续假装增量同步成功,而是要标记为需要重建索引。

这是一个很重要的工程判断:USN Journal 不是永久日志,它可能被系统清理,也可能重建。索引器必须承认这一点。

第八步:处理新增、修改、重命名和删除

USN 记录里有reason字段,用来描述变化原因。项目当前主要处理:

  • 新增或修改:更新或插入对应FileEntry
  • 重命名:忽略旧名称记录,使用新名称记录刷新路径
  • 删除文件:从索引中移除该文件
  • 删除目录:移除该目录以及所有子节点

删除目录是一个容易忽略的细节。如果只删除目录本身,目录下的文件还会残留在索引里,搜索结果就会出现不存在的路径。

因此项目里维护了children_by_parent这类结构,删除目录时会收集整棵子树:

folder ├─ a.txt ├─ b.txt └─ sub └─ c.txt

如果folder被删除,索引里要一起删除a.txtb.txtsubc.txt

第九步:快照持久化

索引建立完成后,如果下次启动还要重新读一遍 MFT,体验会很差。因此项目会把索引状态保存成快照:

pubstructIndexSnapshot{pubversion:u32,pubvolume_state:VolumeIndexState,pubentries:Vec<FileEntry>,}

保存方式是:

IndexSnapshot -> bincode serialize -> 写入临时文件 -> rename 成正式快照文件

用临时文件再 rename,是为了降低写坏快照的概率。如果程序在写入过程中退出,至少不会轻易破坏旧快照。

快照里还带了version字段。以后如果索引结构变化,可以通过版本号判断旧快照是否还能使用。

第十步:后台监听和并发控制

Windows API 调用会阻塞线程,所以项目里把建索引和读 USN 的操作放进spawn_blocking

索引状态使用:

RwLock<SearchIndex>

这样搜索可以读锁访问,建索引或同步变更时再拿写锁。

同时,建索引、同步、保存快照之间不能随意并发执行,否则容易出现状态互相覆盖。因此项目里还有一个操作门禁:

tokio::sync::Mutex<()>

后台监听每隔一段时间读取 USN Journal:

idle -> watching -> syncing -> watching

如果出错,会进入 error 状态,并使用指数退避重试。快照保存也不是每次变化都立刻写磁盘,而是做了节流,避免频繁写入。

Rust 实现里的几个收获

1. unsafe 不可怕,但边界要小

调用 Windows API 时无法完全避免unsafe。关键是不要让unsafe扩散到整个项目。

我的处理方式是:

  • windows_ntfs.rs负责接触原始 Windows API
  • 解析完成后转成 Rust 自己的结构体
  • 其他模块只处理安全的 Rust 类型

这样后续索引、搜索、快照逻辑都可以保持普通 Rust 代码。

2. 系统 API 返回的数据不等于应用数据

USN 记录不是可以直接展示的数据。它更像文件系统内部的变化记录。

应用真正需要的是:

  • 完整路径
  • 文件名
  • 是否目录
  • 扩展名
  • 修改时间
  • 文件大小
  • 可搜索文本

所以中间必须有一层转换,把底层记录整理成应用层索引。

3. 增量同步必须处理失效场景

很多人第一次设计增量同步时,会默认日志永远完整。但 USN Journal 不是这样。

如果 Journal 被系统清理,或者 Journal ID 变化,上次的next_usn就不再可靠。这时继续同步会导致索引悄悄不一致。

正确做法是明确标记:

needs_rebuild = true

然后提示用户重建索引。

4. 删除目录比删除文件复杂

文件删除只需要移除一个节点。目录删除则需要移除整棵子树。

这要求索引不仅能通过file_ref找到条目,还要能通过parent_ref找到所有子节点。

5. 快照比想象中重要

首次索引可以慢一点,但每次启动都慢就不行。快照让索引器从“实验代码”更接近“可用工具”。

保存快照时还要注意:

  • 结构版本
  • 临时文件写入
  • 原子替换
  • 退出前刷新
  • 后台同步后的节流保存

当前限制

FilePulse 目前还是一个 MVP,限制也比较明确:

  • 只支持 Windows
  • 只支持本地固定 NTFS 卷
  • MFT/USN 读取通常需要管理员权限
  • 还没有做 Windows Service
  • 当前搜索语法比较简单
  • 文件大小和部分 metadata 仍需要额外补全
  • 还没有针对百万级结果做虚拟列表展示

这些限制并不影响它作为一个 Rust 系统编程练习项目的价值。相反,正是这些限制让后续演进方向很清晰。

后续计划

后面我想继续完善几个方向:

  • 增加 Windows Service,让索引常驻后台
  • 支持更丰富的查询语法,比如ext:path:is:dir
  • 优化大索引下的搜索性能
  • 对搜索结果异步补全文件大小、修改时间等 metadata
  • 做托盘、全局快捷键和开机启动

总结

这个项目最大的收获,不是做出了一个完整的文件搜索软件,而是用 Rust 走通了一条更底层的 Windows 文件索引路线:

NTFS MFT -> USN Journal -> Rust 数据结构 -> 内存索引 -> 增量同步 -> 本地快照

相比递归遍历目录,这条路线更贴近文件系统本身,也更能体现 Rust 在系统工具开发中的优势:性能可控、内存安全、结构清晰,并且可以把危险的 FFI 边界封装在很小的范围内。

FilePulse 现在还只是一个实验型 MVP,但它已经验证了一个方向:用 Rust 做 Windows 本地效率工具的可行性。

http://www.jsqmd.com/news/1169860/

相关文章:

  • 不掉屑卫生纸哪家性价比高:联盛森宝专业一流 - MXyuyu
  • 别踩 2026做B站视频总结的常见误区:我总结了控成本实操经验
  • 如何在老旧Android电视上获得流畅的4K直播体验?mytv-android原生解决方案
  • 2026年工业胶带厂家推荐榜:德莎/tesa胶带,汽车线束胶带,耐高温布基胶带,水溶性飞接胶带,泡棉双面胶带源头工厂详解 - 甄选服务推荐
  • 2026年真人数字人制作平台选购攻略:3 步筛选法,选出适配靠谱工具
  • SenseNova-U1-8B多模态模型:从信息图生成到一体化AI工作流
  • OpCore-Simplify:15分钟完成黑苹果EFI配置的终极图形化工具
  • 分布式系统中的幂等性设计:消息去重、状态机与唯一键约束实践
  • 编译原理习题实战:3道DFA构造与最小化题解(附状态转移图)
  • RE2-Rust API 完全指南:从 FullMatch 到 Set::Match 的完整使用教程
  • MATLAB 线性方程组求解实战:5种方法(\、inv、pinv、linsolve、solve)精度与效率评测
  • Kimi Claw深度避坑指南:解析器架构缺陷与可靠替代方案
  • MCP3428与PIC18F2525高精度数据采集系统设计
  • GO_复合类型---指针
  • 高压安全隔离技术:ISOM8710与MK51DN512CLQ10应用指南
  • 创业初期的技术招聘策略:如何用股权和愿景吸引优秀工程师
  • 2026全国高压熔断器品牌综合测评与选型分析
  • 2026徐州黄金回收实地实测!市场行情解析+靠谱门店交易全过程复盘 - 微城市网络
  • 劳力士官方保养地址专业售后保养服务权威公示(2026年7月最新) - 劳力士服务中心
  • GTC泽汇资本:把产品理解成本做到位——路径拆解与提示整理
  • 信息学奥赛一本通 1162 题:4 种解法对比与递归思维训练(含复杂度分析)
  • AI代码助手如何重构CI/CD管道?——2024年头部科技公司落地实测数据首次公开(含MTTR下降42%验证报告)
  • GBFR-Logs:碧蓝幻想Relink终极DPS统计工具使用指南
  • STM32锂电池组电压监测与主动均衡系统设计
  • 哪些坑别踩:这 5 类场景现在还不适合上 AI 知识库
  • GO_复合类型---切片
  • 如何统一管理多品牌RGB设备:OpenRGB开源灯光控制完整指南
  • 工业级负载控制方案:TPD2015FN与PIC18F47J53应用解析
  • 面试官:Agent上线后行为失控,你怎么管?我:加规则!他:就这?
  • OpenSSL 3.0 实战:5种常见SSL/TLS握手失败告警的抓包复现与诊断