正文
大家好,我是 Rain9。
先上链接,后面都是干货:
- GitHub:github.com/you-want/Lu… (MIT,免费)
- 官网:luma.raingpt.top/
- 下载:github.com/you-want/Lu… (macOS universal + Windows x64)
Luma
Luma 是一个本地优先的文件空间分析与整理工具:
- 扫描你选定的目录,只读元数据,在本机建 SQLite 索引
- 空间分类、大文件、重复文件候选、开发项目识别、历史对比
- FTS5 + BM25 的本地搜索,支持"大于 1GB 的最近一年的 PDF"这类确定性条件
- 文件管理器支持重命名、移动、复制、废纸篓
- 规则整理走 dry-run:先生成完整清单,你确认才动手,事后可撤销(重启也能撤)
- 中英文界面,macOS / Windows 双平台
没有 AI,不上传任何东西,也没有后台常驻。今天主要聊聊功能设计和开发中踩的坑。
为什么做这个
市面上的磁盘工具大多分两类:一类只能"看",看完大文件还得自己一个个去处理;一类敢"动手",但让一个工具直接动我的文件,我是拒绝的——它凭什么保证不出事?
所以 Luma 的核心思路就一句话:**把"看清楚"和"动手"放进同一个工作区,中间隔着一层预览和确认,动手之后还能反悔。**
功能速览
扫描与空间分析
选一个目录递归扫描,只读元数据(名称、扩展名、大小、修改时间),不跟随符号链接,可以随时取消。单个文件没权限就计入错误数,不会中断整个任务。
扫完有分类分段条、Top 20 大文件、"值得留意"发现(超大文件、长期未动、开发构建内容、压缩包、安装包五类)。每一类都能下钻看明细,汇总数字和明细列表走的是同一套判定规则,不会出现"总数 32 个点开只有 25 个"的尴尬。
搜索:FTS5 + BM25 + 确定性条件
关键词走 SQLite FTS5 external-content 索引,BM25 排序,文件名权重高于路径。中文和特殊字符走安全的字面量匹配兼容路径——用户输入永远不会被当成 FTS 操作符。
比较有意思的是条件解析:
"大于 1GB 的最近一年的 PDF" → size_bytes > 1GB AND mtime >= 一年前 AND extension = 'pdf'
注意这是纯规则解析,不是大模型——它把自然语言条件变成绑定参数和静态 SQL,可解释、可测试。解析器只认一组确定性模式(大于/小于 + 容量单位、最近一年、PDF、下载目录、代码项目),解析不出来的部分就当普通关键词,不会瞎猜。
重复文件:先比大小,再算哈希
先按文件大小筛出候选,再对候选算 BLAKE3 内容哈希,只对确认同内容的文件分组展示。可节省空间是按组去重后算的,不会把两个 10MB 重复文件算成 20MB。同样,只展示候选,不自动删。
规则整理:dry-run → 确认 → 可撤销
这是整个产品的核心链路。提供桌面 / 下载预设方案,也可以按类型、修改日期、扩展名、文件名关键词归类。流程是:
- 生成完整 dry-run 计划(预览阶段不碰文件系统)
- 预览清单:原路径 → 目标路径、归类依据,可以排除单个文件、改分类名,同名冲突选跳过或自动编号
- 确认后执行,成功项逐条写入 SQLite 操作日志
- 任何时候可撤销,撤销记录持久化,重启应用后依然有效
本地存储管理
v0.4.1 加的:Luma 自己的索引也占磁盘,所以应用里可以直接看索引占用、回收空闲空间、清理扫描索引。策略是只保留最近一次成功扫描的完整文件索引,历史扫描降级为统计摘要;单次扫描的索引写入有 256 MiB 预算,超限保留上次成功结果。一个管存储的工具,自己也得管好自己的存储。
工程结构
前端 React 19 + TypeScript + Vite,useSyncExternalStore 轻量 store,i18next 双语
后端 Rust + Tauri 2
scanner.rs walkdir 遍历 + 分类
database.rs SQLite schema(v5)、FTS5、查询
search.rs SearchProvider 抽象 + 条件解析
duplicates.rs 大小预筛 + BLAKE3
file_ops.rs 文件操作 + 操作日志 + 撤销状态机
organizer.rs 规则整理 dry-run
cleanup.rs 清理建议
存储 rusqlite(bundled SQLite),FTS5 external content + 触发器同步
扫描在 Rust 阻塞任务里跑,文件按 500 条一批写入 SQLite,进度约 100ms 节流一次通过事件推给前端。前端不加载完整文件表,分页和聚合都在数据库做。
性能基线(本机 debug、10 万条合成记录,仅供参考不构成承诺):索引写入约 16.6s,关键词搜索约 4.5ms,数据库约 31 MiB。
踩坑实录(这部分最有价值)
坑 1:中文路径 + SQLite SUBSTR,字节数和字符数打架
目录树查询需要用"父路径长度"去 SUBSTR 截出相对路径。我最初用 Rust 的 str.len()——它返回字节数;而 SQLite 的 SUBSTR 按字符数截。ASCII 下两者相等,所以英文测试目录全绿。
换成中文路径就露馅了:/Users/me/项目/child/file.txt 应该截出 child/file.txt,实际截出了 d/file.txt。
修复很朴素:parent_path.chars().count(),然后补上中文、emoji、同前缀目录的边界测试。教训是:涉及路径的字符串运算,字节和字符必须全链路统一口径,测试夹具里必须有非 ASCII 路径。
坑 2:LIKE 前缀没转义 % 和 _
拼接 path LIKE ?/ 前缀时没转义通配符,虽然写了 ESCAPE '\',参数自己不带转义等于白写——目录 /work/a_c 的查询会匹配到 /work/abc。这不是注入,是查询语义错误,更隐蔽。修法是入库前统一 escape_like()。
坑 3:撤销中断后,记录永久卡死
撤销开始时会把操作状态写成 undoing。如果进程恰好死在这个窗口:启动恢复逻辑只处理 running,不认 undoing;再试撤销又撞上 OP_BUSY;可撤销列表也不显示它。一条记录就这么永久卡住了。
修复是加了 reconcile_interrupted_operations():启动时把 running 和 undoing 都标记为 interrupted,而撤销实现本身是逐项持久化的,所以中断的撤销可以安全续跑。配套写了一个"模拟 kill 在状态更新之后"的测试。教训:文件系统操作和 SQLite 事务不是原子的,任何状态机都要考虑"死在两个状态之间"。
坑 4:"可释放约 30MB" 的数学问题
清理建议面板最初把各类建议的体积直接相加:两个相同的 10MB 压缩包,"压缩包"算 20MB,"重复候选"再加 10MB,合计"可释放约 30MB"——实际文件只有 20MB,删掉一个副本最多省 10MB。
分类会重叠,估算不能直接求和。现在文案改成了"待复查文件约 N",口径收敛到扫描范围内的并集,语义从"承诺能省多少"退到"这些值得你看看"。工具类产品,文案的确定性不能超过证据。
坑 5:Windows 保护路径硬编码了 c:\
系统目录保护最初匹配 c:\windows 这类前缀——Windows 装在 D 盘的用户直接失去保护。改成从路径自身解析盘符再做盘符不敏感的匹配,加了专门测试。这类"在我机器上没问题"的平台 bug,只能靠把平台差异显式写进测试。
安装与更新
安装包尚未做平台代码签名和公证(macOS Developer ID + notarization 在计划里),所以首次打开会被 Gatekeeper 拦一次:
- macOS:系统设置 → 隐私与安全性 → 「仍要打开」
- Windows:SmartScreen → 「更多信息 → 仍要运行」
放行一次之后不会再拦。README 里有完整引导。
应用内自动更新走 Tauri updater,更新产物有独立签名校验(和平台签名是两回事),已经有过一次真实的跨版本升级验证(v0.3.2 → v0.4.0)。
明确不做的事
划边界和做功能一样重要:
- 不做后台自动整理、不自动删除、没有永久删除(最多进废纸篓)
- 不分析文档语义——分类依据是文件名、类型、修改时间这些可解释的规则
- 暂不做 AI:等真实使用中出现"固定规则覆盖不了"的场景再说,来了也只做"把意图解析成结构化查询",不直接执行文件操作
最后
项目从 8 月开始写,最近刚发到 v0.4.1,质量门禁是前端测试 + Rust 测试 + fmt + clippy + 双平台 CI 构建全过。
Windows 真机验收还在完善,用出问题欢迎提 Issue;觉得有意思的话点个 Star,比什么都鼓励开发者 🌟
本文由 AI图鉴自动抓取并翻译,版权归原作者所有;如需查看原文请点下方链接。
Text
Hello, I'm Rain9.
The link first, there's dry stuff at the back:
- GitHub: Github.com/you-want/Lu... (MIT, free of charge)
- Official: luma.raingpt.top/
- Download: Github.com/you-want/Lu... (macOS United + Windows x64)
Luma
Luma is a local priority document spatial analysis and collation tool:
- Scan your selected directory, read only metadata, build SQLite index in this machine
- Spatial classification, large documents, candidates for duplicate documents, development project identification, historical comparisons
- Local search for FTS5 + BM25 which supports certain conditions such as "PDF of the latest year greater than 1GB"
- File manager supports renaming, moving, copying, scrap paper
- The rules are sorted out. Mr. Dry-run completes the list, you confirm it and you can withdraw it afterwards.
- Chinese/English interface, MacOS / Windows dual platform
No AI, no uploads, no backstage permanent. Today, the main talk is about the pits of functional design and development.
Why are you doing this?
Most of the disk tools on the market are in two categories: one that can only "see" and one by one, and one that can "do it" but let a tool move my document, and I refuse -- on what guarantee is it that nothing happens?
So Luma's core line is: ** Put "see clearly" and "do it" in the same workspace, with a preview and confirmation between them, and then reverse it. **
Function Snap
Scan and spatial analysis
Select a directory that can be deleted at any time without following a symbol link, read only metadata (name, extension, size, change time). An error is recorded without permission in a single file and the task will not be interrupted.
Discrepancies have been completed, Top 20 large documents have been found, "are worth noting" (super documents, long-term unmoveable, development of construction content, compressed packages, installation of packages in five categories). Each class can go down to see the details, the sum of the numbers and the list of details go by the same set of rules, without the awkwardness of "32 points open to only 25".
Search: FTS5 + BM25 + Requirements for certainty
Keywords go SQLite FTS5 index, BM25 sorted and file name weights above path. Chinese and special characters walk secure text matching compatible paths - user input will never be used as an FTS operator.
It's interesting to see the conditions:
"PDF for the last year greater than 1GB" → size bytes > 1GB AND mtime > = a year ago AND extension = 'pdf'
Note that this is a purely rule-based resolution, not LLM, which transforms natural language conditions into binding parameters and static SQLs that can be explained and tested. The solver recognizes only a set of certainty modes (greater/less than + capacity units, the latest year, PDF, download catalogues, code items) and the undefeated part is a normal keyword and does not guess.
Duplicate file: First scale, then Hashi
Candidates are sifted by document size, then candidates for BLAKE3 content Hashi are shown only in groups of documents confirming the same content. The saving space is calculated by group and does not count two 10MB duplicate files as 20MB. Similarly, only candidates are presented and not automatically deleted.
Rules: dry-run → confirm → can be revoked
This is the core chain of the entire product. Provides a desktop / download preset or can also be classified by type, date of change, extension, filename keyword. The process is:
- Generate a full dry-run plan (without touching the file system at the preview stage)
- Preview list: original path → target path, grouping basis, which excludes single files, recategorizes, conflicts skipping or automatically numbered
- Execute after confirmation, successful entry by article in SQLite Operations Log
- Can be revoked at any time, the record of the cancellation is permanent and will remain valid once the application is restarted
Local Storage Management
v0.4.1 Add: Luma ' s own index also accounts for disks, so that the application can look directly at index occupancy, recovery of free space, and clean-scan indexing. The strategy is to keep only the full file index of the last successful scan, downgraded from historical scan to statistical summary; the index of the single scan is written in the 256 MiB budget and the last successful result is retained beyond the limit. A tool for storage, you have to manage your own storage.
Project structure
frontendReact 19 + TypeScript + Vite, using SyncExternalStore
Backend Rust + Tauri 2
Scanner.rs walkdir + class
Database.rs SQLite Schema(v5), FTS5, Query
Abstract + condition resolution
duplicates.rs Size Prescreen + BLAKE3
file ops.rs file operation + Operations log + Undo status
You can't have to do this, but you can't do this.
Cleanup proposal
Storage rusqlite (bundled SQLite), FTS5 external trigger + sync
Scanning runs in a Rust blocker task, and files are written to SQLite in 500 batches, progressing about 100 ms once to the front. The full file sheet is not loaded at the front end and both the breakout and the aggregation are done in the database.
Performance baseline (inline debug, 100,000 composite records for reference purposes only, does not constitute a commitment): The index is written in about 16.6s, keyword search about 4.5 ms, database about 31 MiB.
It's the best part.
Pipe 1: Chinese Path + SQLite SUBSTR, byte Number and Character Number Fighting
The directory tree query needs to use the parent path length to intercept the relative path of SUBSTR. I started with the str.len() of Rust -- it returns byte numbers; and SQLite's SUBSTR counts by character. ASCII is the same, so the English test catalogue is green.
In Chinese, the path is open: /Users/me/Project/child/file.txt should intercept child/file.txt and actually d/file.txt.
The restoration was simple: parent path.chars().count() was then supplemented by a border test in Chinese, emoji, suffixed directory. The lesson is that a string operation involving a path, byte and characters must have a single calibration of the entire chain and non-ASCII paths in the test kit.
Pit 2: LIKE prefix not converted % and
Collapse path LIKE? / prefix without a transliterator, although ESCAPE '\ ', the parameter itself is not transliterated to white writing - directory /work/a c queries will match /work/abc. It's not injection. It's semantic error. It's more subtle. The amendment is a uniform before entry.
Pit 3: Record of permanent death after interruption
Undoing starts with an undoing status. If the process happens to die in this window: Starts the recovery logic only to run and does not recognize undoing; tries to undo and crash OP BUSY; can cancel the list and do not show it. A record is stuck forever.
The fixation was added (reconcile interrupted operations): Starting with running and undoing is marked as interrupted, while undoing is per se durable, so the aborted revocation can run safely. It's accompanied by a test of "Simulation Kill After Status Update." Lessons learned: File system operations and SQLite services are not atoms, and any status machine considers "dead between two states".
Pocket 4: The mathematical problem of releasing about 30MB
The cleanup panel initially added the recommended sizes directly: two identical 10MB compression packages, 20MB for "compressor" and 10MB for "repeated candidate" and, in aggregate, about 30MB for release... - The actual file is only 20MB, deleting a copy and saving up to 10MB.
Classifications overlap and estimates cannot be calculated directly. Now it's called "Prevalent Document N" and it's condensed into the scanning range, and the semantics go from "how much promises can save" to "these deserve your attention." In the case of tools, the certainty of the case cannot exceed the evidence.
Pipe 5: Windows protected path coded hard
System directory protection initially matches c:\windows such prefixes - Windows is directly unprotected. Replaces it with an unsensitious match from the path's own console, with a special test. This kind of platform, "No problem on my machine," bug, can only be tested by writing the platform difference.
Install & Update
The installation package has not yet been signed for platform code and notarized (macos Developer ID + notariation is planned), so the first opening will be stopped once by Gatekeeper:
- MacOS: System settings, privacy and security, still open.
- Windows: SmartScreen
I won't stop once. README has complete guidance.
The application automatically updates Tauri updater, with an independent signature verification of the product (as opposed to the Platform signature) and a genuine cross-version upgrade verification (v0.3.2 → v. 0.4.0).
It's something you don't know.
Boundary demarcation is as important as functional:
- Do not automatically organize backstage, do not automatically delete, do not permanently delete (up to waste paper)
- Do not analyse the semantics of the document - classification is based on the explanatory rules of file name, type, time of modification
AI: When there's a scenario in real use that's not covered by fixed rules, then there's only a "dissolve intent into a structured query" and no direct file execution.
Finally.
The project starts in August and has just recently been sent to v0.4.1: Quality Gate Ban is the front-end test + Rust test + fmt + clipy + Double Platform CI built all over.
Windows, the real acceptance check is still perfect, and with questions, welcome Issue; if it's interesting, ask Star, and encourage developers more than anything else.
Auto-fetched and translated by AI Atlas. All rights belong to the original author.