☰
MooTool 批量重命名(Batch Rename):跨平台安全执行、冲突拦截与崩溃恢复机制全解析
2026/10/17 2:13:19 网站建设 项目流程
  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载

MooTool 的 Electron 版本在每日工具(Daily Tools)分组中内置了批量重命名(Batch Rename)功能,用于一次处理最多 500 个普通文件。本文以 next/doc/batch-rename.md 为骨架,结合 批量重命名契约定义、主进程服务实现、工具界面 以及 单元测试、端到端测试,完整讲解其文字替换规则、预览/执行模型、跨平台文件名校验、基于硬链接的两阶段移动、预检-回滚-日志恢复三件套,以及预览令牌与撤销记录的生命周期。

读完本文,你将掌握该工具从「选择文件 → 配置规则 → 预览 → 执行 → 撤销/恢复」的完整操作路径,并理解它为什么能做到「预览不写盘、执行不覆盖、中断可恢复」——这些机制同样可以作为你自己文件操作工具的设计参考。

1. 入口与基本使用流程

在 MooTool 中,批量重命名属于**每日工具(daily)**分组,与时间转换、留言板、翻译、计算器等并列。它不支持历史记录(supportsHistory: false),但完整支持命令面板搜索、自定义分组和可脱离主窗口的 Live 工具窗口(见 next/src/app/toolRegistry.ts)。

打开方式有两种:

  • 在工具分组中点击批量重命名;
  • 按下Cmd/Ctrl+K调出命令面板,输入rename或重命名(注册关键字还包括batch、files、批量、文件)后回车进入。

推荐的操作流程如下:

  1. 选择文件:通过系统原生文件选择器一次最多选择500 个普通文件。再次选择会整体替换当前列表;文件可以来自不同目录,每个文件保留在各自原目录中,不会跨目录移动。
  2. 配置规则:设置查找/替换文本、前缀、后缀以及可选的连续编号。
  3. 预览:勾选要处理的文件,点击预览重命名。编号按列表显示顺序生成,且只对被勾选的文件计数。表格中会展示原文件名、建议新文件名、「无变化」与「冲突」等状态。
  4. 执行:点击重命名文件,应用的一定是与预览完全一致的结果。修改规则或勾选会立即使界面预览失效;重新生成预览会使上一个服务端令牌失效。只要存在任何冲突,整个批次都不会执行;预览过程不写入任何文件。
  5. 撤销/恢复:撤销最近一批可还原最近一次成功批次,应用重启后依然有效;下一次成功批次会覆盖此撤销记录;批次失败时,只要回滚成功,之前的撤销记录仍然保留。

工具界面的工具栏由「选择文件」和「撤销/恢复」两个按钮组成(后者在需要恢复时自动显示为「恢复未完成操作」),规则区由四个文本框(查找/替换/前缀/后缀)、三个开关(区分大小写、保留扩展名、连续编号)以及编号参数(起始、位数、分隔符)构成,预览区展示每个文件的勾选、序号、原名称、新名称与状态列,参见 BatchRenameTool.tsx。

2. 命名规则:契约层如何生成新文件名

所有命名规则的合法性与生成逻辑都集中在 next/src/shared/contracts/batchRename.ts,UI 与主进程共用这份纯函数定义,保证预览与执行结果严格一致。

2.1 规则参数与取值范围

RenameOptions定义了全部规则字段,defaultRenameOptions给出默认值:

字段含义默认值约束(由normalizeRenameOptions校验)
find查找文本''字符串,长度 ≤ 120
replacement替换文本''字符串,长度 ≤ 120
prefix/suffix前缀 / 后缀''字符串,长度 ≤ 120
separator编号分隔符'_'字符串,长度 ≤ 120
caseSensitive查找是否区分大小写true布尔
preserveExtension保留扩展名true布尔
numbering是否启用连续编号false布尔
start起始编号1整数,0 ≤ start ≤ 1_000_000
width编号位数(补零)3整数,1 ≤ width ≤ 8

参数校验失败会抛出batchRename.error.options;对应错误文案「文本不超过 120 字符,编号 0–1000000,位数 1–8」同时出现在 多语言文案表 中。UI 侧输入框也同步限制了maxLength={120}、min/max等约束(见 BatchRenameTool.tsx)。

2.2 文件名生成算法

renameCandidate(name, options, index)是核心生成函数,逻辑如下:

  • 扩展名切分:取最后一个.的位置作为切分点;仅当preserveExtension为 true 且存在非首字符的.时才切分,否则整名作为主干。因此默认情况下只有最终扩展名会被保留,例如Photo.final.JPG只保留.JPG。
  • 文字替换:查找文本中的所有正则元字符(. * + ? ^ $ { } ( ) | [ ] \)会被转义后再构造正则,替换文本以函数形式注入、永远不会被当作正则求值。这就是文档所述「替换文本是字面量,包含$与正则元字符也照常处理」的实现保证。
  • 连续编号:当numbering开启时,拼接separator + String(start + index).padStart(width, '0'),index是文件在勾选列表中的序号(从 0 开始)。
  • 组装:结果为prefix + stem + suffix + number + extension。

契约层单元测试覆盖了典型场景(见 contracts 测试):find: 'photo'(忽略大小写)会把Photo.final.JPG变成new_$1.final_done_002.JPG——其中$1原样保留、编号002来自start=1, width=3且序号为 1;[1]→[2]的方括号元字符被转义后按字面替换;preserveExtension: false时.txt→.md才能生效(默认开启时.txt属于扩展名、不会被替换);无扩展名的.env会被当作纯文件名,加前缀后得到backup_.env。

2.3 跨平台文件名合法性校验

validRenameName采用保守策略拒绝以下名称(契约层逐条判定,超出任一条件即标记invalidName冲突):

  • 空字符串、.、..,以及包含路径分隔符/转义含义的../x、a\b;
  • 含 Windows 禁止字符< > : " / \ | ? *或控制字符\u0000-\u001f;
  • 以.或空格结尾(对应 Windows 的尾部点/空格限制);
  • Windows 保留设备名CON、PRN、AUX、NUL、COM[1-9]、LPT[1-9](忽略大小写,允许带扩展名形式如CON.txt);
  • UTF-8 编码后超过255 字节(new TextEncoder().encode(name).length)。

同时renameNameKey会把名称做 NFC 归一化并转小写,作为「跨平台等价名称」的判定键:在区分大小写、macOS 等大小写不敏感文件系统、以及 NFC/NFD 兼容场景下,Photo.JPG与photo.jpg、或经过不同 Unicode 归一化但视觉一致的名称都会被识别为冲突,避免在目标平台上撞名。

3. 预览、令牌与执行:不写盘的「所见即所得」

3.1 预览不写盘

preview()只做三件事:校验参数、逐文件计算新名称、做静态冲突分析。它不触碰任何文件内容与名称,因此「预览后原文件毫无变化」是严格成立的——服务端单测中expect(await readdir(directory)).toEqual(['a.txt', 'b.txt'])直接断言了这一点(见 batchRenameService.test.ts)。

3.2 预览令牌与失效规则

  • 预览结果附带一个服务端随机令牌token;只有携带该令牌的execute请求才会被接受。
  • 修改规则或勾选(UI 侧任何change()调用)会令界面revision自增并清空本地预览——此时执行按钮必然禁用(见 BatchRenameTool.tsx)。
  • 重新预览会删除该 owner 之前的所有计划,旧令牌随之失效,即使被 UI 外的调用方复用也会被拒绝(fail('preview'))。
  • 令牌与选择授权都归属于打开原生选择器的那个渲染进程(owner 即event.sender.id);当 Live 工具视图被拖出成为独立窗口时,授权依然有效,这由端到端测试中的「分离原生视图」用例验证(见 batch-rename.spec.ts)。

3.3 执行前的二次预检(preflight)

execute()在写入任何内容之前,会再次执行preflight(plan.moves):

  • 源文件身份比对:重新读取每个源文件的 device/inode/size/mtime,若与预览授权时记录的身份不一致则判定changed——即使文件系统复用了 inode 号,大小或修改时间的变化也能暴露「文件已被替换」,对应错误batchRename.error.changed(文件已移动或被替换)。
  • 目标占用复查:重新扫描目标目录,若发现任何不在本批次源集合中的同名条目则判定occupied,绝不覆盖外来文件。这保证了「预览之后、执行之前」被第三方创建的目标文件不会被误写。

3.4 执行按钮与「已选/待改名」计数

工具栏下方会实时显示已选 N 个 · 待改名 M 个(batchRename.counts,来自预览的changedCount)。执行按钮仅在「预览存在且令牌有效、无冲突」时可用;存在冲突时界面显示红色提示「存在名称或文件冲突,本批不会执行」,且整批被拒绝(token为 null),而不是跳过冲突文件继续。

4. 底层执行模型:硬链接 + 两阶段暂存,绝不覆盖

文档的 Boundaries 部分强调「执行写入步骤使用独占式硬链接创建后再删除旧名」,这是本功能安全性的核心。对应实现位于 batchRenameService.ts:

private async move(source: string, target: string, expected: Identity): Promise<void> { const current = await this.identity(source) if (!current || !matches(current, expected)) fail('changed') // link() creates exclusively; unlike rename(), it never replaces a destination. await this.fs.link(source, target) const after = await this.identity(source) if (!after || !matches(after, expected)) fail('changed') await this.fs.unlink(source) } private async run(moves: Move[]): Promise<void> { for (const move of moves) await this.move(move.source, move.temporary, move.identity) for (const move of moves) await this.move(move.temporary, move.target, move.identity) }

要点如下:

  1. 独占创建:link(source, target)在目标已存在时会失败(EEXIST),因此任何已存在的目标文件都绝不会被覆盖——这与rename()不同,后者会静默替换。测试专门注入了一个「在执行瞬间向目标路径写入外来内容」的故障(见 batchRenameService.test.ts),验证外来文件被保留、源文件完整回滚。
  2. 两阶段暂存:先统一把每个源文件移动到原目录下的.mootool-rename-<uuid>临时名,再统一移动到最终目标名。这使两类棘手场景成为可能:
    • 仅大小写变化(如a.txt→A.txt),在不区分大小写的文件系统上先移开旧名再写入新名;
    • 目标恰好是同一批次中另一个文件的旧名(destination vacated by the same batch),例如把a.txt改成aa.txt、aa.txt改成aaaa.txt的链式移动。
  3. 身份守恒:每次move前后都会重新比对身份(dev/ino/size/mtimeNs),移动过程中任何一步源文件被第三方替换都会中止。文件内容与文件身份在整个重命名过程中保持不变——测试断言了ino不变、内容alpha/beta原样保留(见 batchRenameService.test.ts)。

4.1 对文件系统的硬性要求

文档明确指出边界:执行依赖硬链接且目录必须可写。当目标文件系统抛出ENOTSUP、EOPNOTSUPP、EXDEV、EPERM时(分别对应不支持硬链接、跨设备、权限不足),服务会回滚本批并抛出unsupported错误,文案为「文件系统不支持硬链接或没有写入权限,本批文件已恢复原名」。实现不会退化为复制或覆盖。对应测试构造了link抛ENOTSUP的场景,验证原文件内容不变、此前的撤销记录也依然可用(见 batchRenameService.test.ts)。

4.2 同 inode 检测

多个被选中的硬链接名称指向同一 inode 时(例如a.txt与alias.txt互为硬链接),预览会把这些行全部标记为sameFile冲突——否则同一内容会被移动两次、产生名称丢失。检测通过dev:ino作为键对批次内行做去重实现(见 batchRenameService.ts)。

5. 冲突类型速查

预览表中的状态与冲突issue字段映射如下(对应 UI 展示与文案见 messages.ts):

状态/冲突含义触发条件(源码判定位置)
ready待改名新名称与旧名不同且全部检查通过
unchanged无变化新名称等于旧名称,该行不计入changedCount
invalidName无效文件名validRenameName判定失败(见 2.3 节)
duplicate新名称重复多个文件在批内生成同一目标键
occupied已有同名文件目标目录已存在不在本批源集合中的同名条目
changed文件已移动或被替换预览时/执行前身份不匹配
sameFile多个名称指向同一文件批内出现相同dev:ino

任何一行冲突都会导致整批不产生令牌,执行按钮保持禁用。

6. 撤销、崩溃恢复与日志

6.1 两代撤销链

BatchRenameService维护一份应用级(全局)日志:<userData>/file-operations/batch-rename-journal.json(路径在 index.ts 中由app.getPath('userData')拼接而来)。日志以原子方式更新:

  • saveJournal先把新内容写入batch-rename-journal.json.<uuid>.tmp(flag: 'wx',权限0o600),再通过rename原子替换旧文件(见 batchRenameService.ts);
  • 日志结构为{ version: 1, phase: 'pending' | 'done', moves[], previous? },previous指向上一代已完成批次,从而形成「上一批 → 当前批」的两层撤销链。

执行流程:

  1. 预检通过后,先把当前批写入pending日志(同时把上一代塞进previous);
  2. 执行两阶段移动;成功后把日志改写为phase: 'done';
  3. 失败时调用rollback(journal)尽力恢复原名;回滚成功则保留previous日志——即失败的批次不会吞掉上一批的撤销记录;
  4. 撤销 = 以target → source方向重放日志中记录的 moves,恢复成功后删除日志。

因此「撤销最近一批恢复的是最近一次成功批次,应用重启后依然有效;下一次成功批次会覆盖该撤销记录」这一文档描述,正是由previous链与重启后readJournal()的支持共同保证的。单元测试专门「重启服务」(new BatchRenameService)后验证undoAvailable与内容恢复(见 batchRenameService.test.ts)。

6.2 中断恢复(Recover unfinished operation)

当进程在移动中途崩溃或异常退出,磁盘上会残留pending日志、部分源文件可能暂处于.mootool-rename-<uuid>名称之下。重启后:

  • status()检测到phase === 'pending'即置recoveryRequired: true,所有新的预览被阻止(fail('recovery'));
  • UI 的撤销按钮变为「恢复未完成操作」,并给出日志路径提示;
  • 恢复 = 基于日志按 inode 身份定位每个文件当前所在位置(source / temporary / target),把能确认属于本操作的文件全部归位;若出现冲突(如原名被占用)则报告冲突而不替换该文件,恢复未完成时日志保留、可重复尝试。

恢复逻辑对「损坏日志」非常保守:日志文件必须满足版本号、phase、绝对路径、临时名模式(^\.mootool-rename-[\da-f-]{36}$)、数字身份字段等全部校验(见 batchRenameService.ts),一旦损坏/格式非法即fail('journal')停止访问该记录,不会静默丢弃——错误文案明确提示「请保留应用数据目录中的 file-operations/batch-rename-journal.json 以便检查」。

文档还提示了若干恢复边界,值得如实说明:

  • 撤销/恢复只还原文件名,不会回滚文件内容编辑;
  • 若文件被外部移动/替换、或原名被外部占用,撤销可能无法完成(测试refuses undo when an original name has been taken验证了此时两个文件都完整保留,见 batchRenameService.test.ts);
  • 这些校验并非针对其他应用并发修改路径的文件系统锁,无法保证与外部进程的强互斥。

6.3 服务端串行化与权限隔离

服务用一把busy锁串行化 select/preview/execute/undo 全部操作,并发请求会得到busy错误。所有 IPC 通道(batch-rename:select / preview / execute / undo / status)都经过assertToolWindowAccess(event.sender, 'batchRename')校验渲染进程归属,且execute/preview只接受文件 ID 与令牌而非任意路径——「渲染进程请求无法提供任意的重命名源/目标路径」,从根上杜绝了主进程被执行不可信路径操纵(见 index.ts 与 preload/index.ts)。

7. 工具集成与「不进 MCP / 不进文本操作」的设计取舍

批量重命名工具:

  • 参与命令面板搜索、自定义分组、Live 可脱离工具窗口(tool('batchRename', 'daily', ...),关键字含中英文,见 toolRegistry.ts);
  • 因涉及文件写操作,它不作为快捷文本操作暴露,也不通过 MCP 暴露——MooTool 的 MCP 工具面(next/electron/mcp/server.ts)只暴露 vault 等纯查询/沙箱能力,不会让外部 Agent 触达文件重命名;
  • 界面侧还带有WorkspaceDragZone工作区拖放外壳与完整的错误/提示横幅(batchRename-error、batchRename-notice、恢复提示含日志路径),并在恢复状态下禁用预览与执行按钮。

8. 验证体系:单元测试 + 端到端 + 全量 check

8.1 验证策略

文档强调所有校验只使用临时 fixture,不会污染真实文件系统。两类测试各司其职:

  • 服务层单元测试next/electron/main/batchRenameService.test.ts:覆盖 owner/令牌隔离(跨 owner 的 preview/execute 一律拒绝)、名称冲突(duplicate/occupied/invalid/sameFile)、被替换的源文件、仅大小写重命名、选中目标成链移动、预检后目标被占、回滚、重启恢复、硬链接不受支持、撤销时原名被占等全部路径;
  • 契约层单元测试next/src/shared/contracts/batchRename.test.ts:覆盖字面替换/扩展名保留/编号拼接与非法名清单(''、.、..、../x、a\b、CON.txt、foo.、foo、nul、a\0b、86 个「你」字超限)以及参数类型/取值范围校验;
  • 端到端 UI 测试next/tests/electron/batch-rename.spec.ts:通过 Playwright 驱动真实应用,验证预览不改盘、修改规则后执行按钮禁用、执行与撤销通知、冲突行展示「已有同名文件」、源被替换时「被替换」告警、重启后撤销可用,以及分离的原生工具视图中完整走通「选择→预览→执行→撤销」(含最小窗口 600×440 布局断言与截图产出)。

8.2 一键全量验证

在next/目录下执行:

npm install npm run check

check串行执行typecheck(tsc --noEmit)、test(vitest 全量单测)与build(生产构建),是验证本功能及其上层构建集成是否完好的标准入口(见 next/package.json)。UI 端到端测试则通过npm run test:e2e运行 Playwright。

8.3 平台现状

文档明确说明:运行时验证已在 macOS 上执行;Windows/Linux 的文件系统与桌面验收仍然待办(pending)。考虑到功能强依赖硬链接语义与各平台文件名规则差异(如 Windows 保留名、大小写敏感性、Unicode 归一化差异),在 Windows/Linux 上使用本功能前,建议先在目标系统跑一遍npm run check与端到端用例并关注其平台差异结果。

9. 总结:这个「普通」的批量重命名为什么不普通

回到 next/doc/batch-rename.md 的定义,MooTool 批量重命名的设计哲学可以概括为四句话:

  • 窄而有界:只做普通文件、只做字面文本替换、只留在原目录,目录递归、符号链接、正则匹配、任意目标目录一概不做;
  • 预览即契约:令牌绑定 owner、规则与勾选变更即失效、执行前二次预检,保证「执行结果 = 预览结果」;
  • 安全优先:硬链接独占创建 + 两阶段暂存,任何情况下都不覆盖已存在文件,内容与 inode 身份全程守恒;
  • 可恢复:原子日志 + pending/done 两相状态 + previous 撤销链,崩溃后能恢复、失败不吞掉上一批撤销记录,损坏日志宁可停用也不静默丢弃。

这套「边界声明 → 契约层纯函数 → 主进程串行服务 → UI 失效模型 → 双测试层验证」的实现路径,正是值得开发者在设计任何本地文件改写型工具时复用的安全模式。相关源码入口:规则契约 batchRename.ts、服务实现 batchRenameService.ts、界面组件 BatchRenameTool.tsx、主进程 IPC 注册 index.ts、预加载 API preload/index.ts。

  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载
上一篇:原神帧率解锁终极指南:5分钟突破60帧限制,畅享丝滑体验
下一篇:grill-me 完整使用指南:如何把模糊想法拷问成能辩护的决策

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询