☰
MooTool Next Compose 备份路径打开机制解析:DIFF-398 如何区分 config 与 data 根
2026/10/15 2:39:20 网站建设 项目流程
  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

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

本篇文章以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_FILEsettings.json设置在configRoot/config/下
DATABASE_NAMEmootool-compose.sqliteSQLite 在dataRoot/下
DATA_DIR_ENVMOOTOOL_COMPOSE_DATA_DIR开发/测试覆盖根的环境变量
PRODUCT_MARKERproduct.jsondataRoot 下的产品指纹,防止选错目录

三平台实际路径

数据、平台与发布 规定了各平台的落盘位置:

数据类型macOSWindowsLinux
配置/引导描述~/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

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载
上一篇:工作性价比计算器:5分钟快速评估你的工作真实价值
下一篇:5个MusicFreeDesktop插件实战技巧:打造你的专属音乐空间

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

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

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

立即咨询