- 桌面应用
- 开发工具
【免费下载链接】MooTool
Handy tool set for developers
本篇文章以next-compose/docs/diff/398-backup-open-config-vs-data.md为主线,深入拆解 MooTool Next Compose(Kotlin Compose Desktop 版本)在“设置备份路径行”中按真实路径打开文件/目录的设计与实现:配置放在configRoot/config/、SQLite 放在有效dataRoot/,而“在文件夹中打开”行为随之细化为“打开 SQLite 文件所在位置”与“打开设置文件所在 config 目录”两条分支。读完你可以掌握该产品配置/数据双根布局的来龙去脉、BackupOpenLocation枚举与pathForOpen的解析逻辑、DesktopAPI 的降级打开策略,以及配套的单元测试与离线验收方式。
问题背景:为什么“配置文件”不能打开 data 根
next-compose/docs/diff/398-backup-open-config-vs-data.md记录了一个真实的路径定位问题。MooTool Next Compose 采用配置与数据分离的目录模型(见 数据、平台与发布):
- 产品设置文件
settings.json位于configRoot/config/; - SQLite 数据库
mootool-compose.sqlite位于有效的dataRoot/下; - 上一个迭代 DIFF-396(见 设置备份路径「在文件夹中打开」)为设置备份路径行加上了“打开”能力,但对“配置文件”行也统一打开了 data 根,导致用户找不到设置文件所在目录。
DIFF-398 的修复思路很明确:Electron 版本的userData把配置与数据合并放在同一个目录下,因此其location: data是合理的;而 Compose 版本既然在磁盘上真实拆分了两类目录,就必须按真实路径打开,而不是沿用 Electron 的合并语义。
配置与数据的双根布局
AppDirectories 的目录约定
路径模型定义在 AppPaths.kt 的AppDirectories中,它同时持有configRoot、dataRoot、cacheRoot、logRoot四个根,并派生出关键文件路径:
data class AppDirectories( val configRoot: Path, val dataRoot: Path, val cacheRoot: Path, val logRoot: Path ) { val settingsFile: Path get() = configRoot.resolve("config").resolve(ProductIdentity.SETTINGS_FILE) val databaseFile: Path get() = dataRoot.resolve(ProductIdentity.DATABASE_NAME) // ... }常量定义集中在 ProductIdentity.kt:
| 常量 | 值 | 含义 |
|---|---|---|
SETTINGS_FILE | settings.json | 设置在configRoot/config/下 |
DATABASE_NAME | mootool-compose.sqlite | SQLite 在dataRoot/下 |
DATA_DIR_ENV | MOOTOOL_COMPOSE_DATA_DIR | 开发/测试覆盖根的环境变量 |
PRODUCT_MARKER | product.json | dataRoot 下的产品指纹,防止选错目录 |
三平台实际路径
数据、平台与发布 规定了各平台的落盘位置:
| 数据类型 | macOS | Windows | Linux |
|---|---|---|---|
| 配置/引导描述 | ~/Library/Application Support/com.rememberber.mootool.next.compose/config/ | %APPDATA%/MooToolNextCompose/config/ | $XDG_CONFIG_HOME/mootool-next-compose/ |
| 默认持久数据 | 同一 Application Support 根下data/ | %APPDATA%/MooToolNextCompose/data/ | $XDG_DATA_HOME/mootool-next-compose/ |
| 缓存/更新暂存 | ~/Library/Caches/... | %LOCALAPPDATA%/MooToolNextCompose/cache/ | $XDG_CACHE_HOME/mootool-next-compose/ |
| 日志/状态 | ~/Library/Logs/MooToolNextCompose/ | %LOCALAPPDATA%/MooToolNextCompose/logs/ | $XDG_STATE_HOME/mootool-next-compose/ |
Linux 的 XDG 变量缺省时回退到~/.config、~/.local/share、~/.cache、~/.local/state,AppPaths.resolve()会读取环境变量并在非绝对或不可写时降级。这正是 DIFF-398 必须区分“配置文件”与“数据文件”的物理基础——两者不在同一个根下。
自定义数据根的“有效 dataRoot”
用户可在设置里配置自定义data.directory。DataPathConfig.kt 的withEffectiveDataRoot(bootstrap, configuredDataDirectory)会校验自定义根为绝对路径,并把dataRoot替换为effectiveDataRoot,而configRoot保持不变。文档特别强调:更改自定义数据根后,固定配置目录保留指向新根的引导描述,重启才找得到。因此备份路径行的 SQLite 路径取的是“有效 dataRoot”,而设置文件始终在configRoot/config/。
BackupOpenLocation 枚举与 pathForOpen 解析
DIFF-398 的落点集中在一个枚举和一个解析函数,全部实现在 BackupInfo.kt。
枚举定义
BackupOpenLocation共有五个取值,对应设置页备份区展示的五类路径:
enum class BackupOpenLocation { /** SQLite under effective `dataRoot`. */ DatabaseFile, /** `config/` next to bootstrap settings. */ SettingsConfig, Images, QuickNote, JsonVault, }pathForOpen 的核心分支
BackupInfo是一个纯数据类(dataDirectory、databasePath、settingsPath、imagesPath、quickNotePath、jsonVaultPath),pathForOpen(location)是 DIFF-398 修改的焦点:
fun pathForOpen(location: BackupOpenLocation): Path = when (location) { BackupOpenLocation.DatabaseFile -> Path.of(databasePath) BackupOpenLocation.SettingsConfig -> Path.of(settingsPath).parent BackupOpenLocation.Images -> Path.of(imagesPath) BackupOpenLocation.QuickNote -> Path.of(quickNotePath) BackupOpenLocation.JsonVault -> Path.of(jsonVaultPath) }关键区别只有一行:SettingsConfig返回settingsPath.parent,即configRoot/config/目录本身;DatabaseFile则直接返回 SQLite 文件路径。图片、QuickNote、JsonVault 三个分支保持不变,仍是目录路径。
BackupInfoResolver.resolve(directories, quickNoteVaultRoot, jsonVaultRoot)负责从AppDirectories与两个 Vault 根组装这些路径,并对所有路径做toAbsolutePath().normalize(),保证展示给用户的绝对路径不含..等冗余段。
openBackupLocation:文件 reveal 与目录 open 的双语义
路径解析完后,真正的“打开”动作由 AppContainer.kt 的openBackupLocation完成,它对不同位置采用了两套打开策略:
fun openBackupLocation(location: BackupOpenLocation) { val target = backupInfo().pathForOpen(location) when (location) { BackupOpenLocation.DatabaseFile -> { runCatching { target.parent?.createDirectories() } revealInFileManager(target) } BackupOpenLocation.SettingsConfig -> { runCatching { target.createDirectories() } openDirectory(target) } BackupOpenLocation.Images, BackupOpenLocation.QuickNote, BackupOpenLocation.JsonVault, -> { runCatching { target.createDirectories() } openDirectory(target) } } }DatabaseFile(数据文件行):目标是 SQLite 文件。打开前先对target.parent做createDirectories()(父目录不存在则创建,保证 reveal 不会失败),然后调用revealInFileManager选中该文件。SettingsConfig(配置文件行):目标是config/目录。创建目录后直接openDirectory打开整个目录,让用户直接看到settings.json与其旁的bootstrap.json。
revealInFileManager本身也有降级逻辑,它优先使用Desktop.Action.BROWSE_FILE_DIR在系统文件管理器中选中文件;若平台不支持或打开失败,则回退为Desktop.open(父目录):
fun revealInFileManager(path: Path) { val file = path.toFile() val desktop = Desktop.getDesktop() runCatching { if (file.isFile && desktop.isSupported(Desktop.Action.BROWSE_FILE_DIR)) { desktop.browseFileDirectory(file) } else { desktop.open(if (file.isDirectory) file else file.parentFile ?: file) } }.recoverCatching { desktop.open(if (file.isDirectory) file else file.parentFile ?: file) } }openDirectory则更简单,直接Desktop.getDesktop().open(path.toFile())。这样“文件行选中文件、目录行打开目录”的差异就完整落到了实现层。
设置页 UI 与 i18n
设置页的备份区由 SettingsScreen.kt 渲染。它通过LaunchedEffect(settings.data.directory, settings.vault.jsonPath, settings.vault.quickNotePath, sessionGeneration)在数据根或 Vault 根变化时自动刷新backupInfo(),再用BackupPathRow逐行展示:
BackupPathRow(container, container.t("settings.backup.database"), info.databasePath, BackupOpenLocation.DatabaseFile) BackupPathRow(container, container.t("settings.backup.settings"), info.settingsPath, BackupOpenLocation.SettingsConfig) BackupPathRow(container, container.t("settings.backup.images"), info.imagesPath, BackupOpenLocation.Images) BackupPathRow(container, container.t("settings.vault.quickNote"), info.quickNotePath, BackupOpenLocation.QuickNote) BackupPathRow(container, container.t("settings.vault.json"), info.jsonVaultPath, BackupOpenLocation.JsonVault)BackupPathRow显示等宽字体的路径文本,右侧是“在文件夹中打开”按钮,点击后调用container.openBackupLocation(openLocation)。文案来自 Translator.kt 的中英文资源表:
| 键 | 中文 | 英文 |
|---|---|---|
settings.backup.database | 数据文件 | Data file |
settings.backup.settings | 配置文件 | Settings file |
settings.backup.open | 在文件夹中打开 | Open in folder |
settings.backup.pathsHint | 下列路径会写入 zip 备份;更改数据目录或文档库后自动刷新 | These paths are included in zip backups and refresh when data or vault roots change |
界面层面不需要感知 config/data 的差异——差异被pathForOpen的枚举分支完全封装,UI 只负责把枚举值传给容器。
测试与验收
DIFF-398 配套更新了 BackupInfoTest.kt,新增对pathForOpen的断言,直接锁定“按真实路径打开”这一修复目标:
assertEquals(dirs.databaseFile.toAbsolutePath().normalize(), info.pathForOpen(BackupOpenLocation.DatabaseFile)) assertEquals(dirs.settingsFile.parent.toAbsolutePath().normalize(), info.pathForOpen(BackupOpenLocation.SettingsConfig)) assertEquals(jsonVault.toAbsolutePath().normalize(), info.pathForOpen(BackupOpenLocation.JsonVault))测试通过AppPaths.resolve(createTempDirectory(...).toString())构造隔离的AppDirectories,并用AppSettings.Default.copy(data = ...)注入自定义数据根与两个 Vault 根,再调用DataPathConfig.withEffectiveDataRoot得到有效目录,最后断言三个位置各自的pathForOpen结果:
DatabaseFile必须解析到dirs.databaseFile(即有效 dataRoot 下的mootool-compose.sqlite);SettingsConfig必须解析到dirs.settingsFile.parent(即configRoot/config/),这正是 DIFF-398 从“打开 data 根”修正为“打开 config 目录”的回归证据;JsonVault继续解析到自定义 Vault 根,证明“图片与 Vault 行不变”。
同一测试还断言databasePath包含mootool-compose.sqlite子串、自定义jsonVaultPath/quickNotePath与输入根一致,覆盖了BackupInfoResolver的组装正确性。
文档标注的验收标准为A01/A03与离线命令desktopTest --offline,即该改动需通过桌面端离线单元测试验收,不依赖真实系统桌面环境。
小结
DIFF-398 是一次典型的“行为跟随真实目录模型”的修正:它没有为“在文件夹中打开”设计复杂的状态机,而是用枚举分支把文件 reveal与目录 open两种语义显式区分——DatabaseFile指向 SQLite 文件本身(父目录不存在则创建),SettingsConfig指向设置文件所在的config/目录,图片与 Vault 行保持原样。配合BackupInfoTest的三条断言,这套设计同时保证了配置/数据分离架构下的路径可预期、可回归、可离线验收,也为后续 DIFF-403(见 设置展示配置根与迁移预览重置)在设置页进一步暴露configRoot打下了基础。
- 桌面应用
- 开发工具
【免费下载链接】MooTool
Handy tool set for developers
相关推荐
MooTool Next Compose 备份路径「在文件夹中打开」:从 `BackupInfo` 解析到 `Desktop.open` 的落地实现
MooTool Next Compose 备份路径「在文件夹中打开」:从 BackupInfo 解析到 Desktop.open 的落地实现 导读 本文围绕 n
桌面应用开发工具MooTool Next Compose Vault Git 中止合并确认机制解析(DIFF-108)
MooTool Next Compose Vault Git 中止合并确认机制解析(DIFF 108) 本文以 MooTool 仓库中 next compose
桌面应用开发工具MooTool next-compose:Host 工具打开查找时如何预填正文选区(DIFF-276 实现解析)
MooTool next compose:Host 工具打开查找时如何预填正文选区(DIFF 276 实现解析) 本文围绕 MooTool 的 Compose
桌面应用开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考