Skip to content

feat: 支持修改启动器数据目录位置(issue #43) - #44

Merged
Gu-ZT merged 9 commits into
dsh-plugins:mainfrom
yukitakasama:feat/relocatable-data-dir
Sep 14, 2026
Merged

Gu-ZT merged 9 commits into
dsh-plugins:mainfrom
yukitakasama:feat/relocatable-data-dir

Conversation

@yukitakasama

Copy link
Copy Markdown
Contributor

概述

支持修改启动器数据目录位置(issue #43)。设置页「数据目录」卡片新增「更改位置」入口,重启后自动把现有数据(配置、版本、HOME 目录、日志、npm 缓存、托管 node 等)迁移到新位置;支持 DSH_LAUNCHER_DATA_HOME 环境变量覆盖;任一步失败自动回滚,不损坏原数据。

变更内容

自举解析链(migrate.rs)

优先级:DSH_LAUNCHER_DATA_HOME 环境变量 > 指针文件 <默认app_data_dir>\data-home.txt > 默认目录。

迁移状态机

迁移在重启后的启动阶段执行(日志文件句柄打开之前),流程:

  1. 目标为空目录才允许迁移;写入 MIGRATION_IN_PROGRESS 标记(power-failure/杀进程安全,中断后下次启动清理半成品重来)
  2. 复制 config.json / versions/ / homes/ / logs/ / .pnpm-store/ / tools/ / icons/ / bin/(纯 std::fs 递归,不新增依赖)
  3. 校验:文件数 + 总大小 + 新 config.json 可解析
  4. 切换并删除标记;旧目录快照为 <dir>.old-<ts>,不直接删除(30 天恢复窗口)
  5. 任一步失败 → 删除目标半成品 + 移除指针 → 回退默认目录正常启动

新命令

  • pick_data_dir — 打开目录选择器(tauri-plugin-dialog)
  • commit_data_dir — 校验新目录(非空 / 是否已存在 / 可写)并写入指针文件,不移动文件
  • get_data_dir_source — 返回当前路径 + 来源(env / pointer / default)+ 兜底提示 notice

UI(i18n 双语)

设置页数据目录卡片:来源标识、迁移失败 toast、更改位置按钮 + 重启确认 Modal。

验证

  • cargo check 零告警
  • 新增 7 个迁移单元测试(路径比较 / 递归复制 / 统计 / 迁移成功 / 拒绝非空目标 / 失败清理)
  • 全量测试 119 passed / 0 failed
  • npx tsc --noEmit 零错误
  • pnpm build 通过
  • tauri build 产出 exe / msi / nsis 三产物
  • 冒烟:DSH_LAUNCHER_DATA_HOME 指向临时目录启动,日志确认数据目录解析到该位置

附加

  • docs/ISSUE43_TASK_TREE.md:任务执行记录(6 阶段逐轮回填)。

yukitakasama and others added 8 commits September 12, 2026 14:59
…sue dsh-plugins#43)

- migrate.rs: bootstrap resolution chain (env DSH_LAUNCHER_DATA_HOME >
  pointer file data-home.txt > default app data dir), one-shot migration
  before the log handle opens: copy config/versions/homes/logs/.pnpm-store/
  tools/icons/bin -> verify -> switch -> snapshot old dir as .old-<ts>.
  Failure rolls back to the default dir and drops the pointer.
- AppState gains data_dir_source + data_dir_notice; commands pick_data_dir
  (blocking folder dialog), commit_data_dir (validate + write pointer),
  get_data_dir_source (path + source + fallback notice).
- lib.rs setup calls migrate::bootstrap before applog::init and
  ensure_local_node_on_path, so logs/ and tools/ move cleanly.
- 4 unit tests for paths_equal/dir_is_empty/copy_tree/tree_stats.
…sh-plugins#43)

- Settings.vue: data-dir card gains a 'Change location' button that opens
  the folder dialog (pick_data_dir), validates + writes the pending pointer
  file (commit_data_dir), then shows a restart hint modal; the card also
  displays the current source ('env' / 'pointer' / 'default') and surfaces
  the startup fallback notice as a toast.
- api/index.ts: pickDataDir / commitDataDir / getDataDirSource wired with
  browser-preview mock branches; types.ts adds DataDirInfo.
- i18n: zh-CN + en-US keys for moveTo/restartHint/migrating/migratedToast/
  source*/rollback/confirmMove.
…cords (issue dsh-plugins#43)

- copy_tree handles file sources (no create_dir_all on files) — fixes
  os error 267 when migrating config.json.
- New tests: migrate_data_dir_copies_and_clears_flag,
  migrate_data_dir_rejects_non_empty_target,
  migrate_data_dir_failure_removes_partial_target.
- Full suite: 119 passed; tauri build produced exe/msi/nsis bundles.
@Gu-ZT

Gu-ZT commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

请求修改(request changes)

自举解析链、指针文件、启动期迁移(早于 applog::init)、按子目录白名单复制、.old-<ts> 快照回退、pick/commit/get_data_dir_source 三件套与 mock 同步——整体架构方向正确,lib.rs 的接缝(AppState.data_dir 仍是不可变字段,48 处消费方零改动)也很干净。但有 1 处数据完整性级缺陷必须修。

阻塞项 1:config.json 里的绝对路径没有随迁移重写 —— 迁移后版本与专属 HOME 全部失效

config.json 存的是绝对路径

  • DshVersion.dirconfig.rspub dir: PathBuf)——版本安装目录
  • DshHome.pathpub path: PathBuf)——每个专属 HOME 的目录

migrate_data_dirconfig.json 原样复制过去,全仓库没有任何一处按新 data_dir 重写这些路径(config.rs 的启动态函数只有 dedupe_homes/cleanup_orphan_homes/ensure_user_dsh_home/migrate_news_sourcelib.rs 也没有 reconcile 步骤)。

后果链:

  1. 用户从 C:\Users\<u>\AppData\Roaming\in.dsh-plug.dsh-launcher 迁到 D:\dsh-launcher
  2. 旧目录被 snapshot_old_dir 重命名为 <old>.old-<ts>,原路径不再存在
  3. config.json 里每个实例的 home.path 与每个版本的 dir 仍指向旧路径 → 实例无法启动(DshHome 不存在 / version_bin_ready 为假,报「版本安装不完整」);
  4. 更糟的是 load_config 里的 cleanup_orphan_homes 会把「路径不存在且无实例引用」的 HOME 记录直接删除,用户会看到 HOME 列表凭空少项,而数据其实躺在 <old>.old-<ts> 里;
  5. 同时 data_dir.join("versions") / data_dir.join("homes")tasks.rs:960/1109commands.rs:78/427)已指向目录 → 新旧路径混杂,用户重新安装版本后 config 里会同时存在两套路径的版本记录。

最小修复(任选其一):

  • 迁移成功后重写 config:把 versions[].dirhomes[].path 中位于 old_dir 下的前缀替换为 new_dirstrip_prefix(old) + join(new)),再 save_config;或
  • 迁移后删除 config 里的绝对路径依赖(改成相对 data_dir 解析),但这会牵动 48 处消费方,建议不做。

无论哪种,都要补一条断言「迁移后 versions[].dir/homes[].path 全部存在于新目录下」的测试——目前 migrate.rs 的 7 个测试里没有任何一条覆盖这个契约(测试用的 config.json"{}",所以完全绕过了该问题)。

阻塞项 2:迁移后每次启动都会新生成一个 .old-* 快照目录(且与文档「30 天窗口」不符)

migrate.rs 的「已迁移」分支(bootstrappointer_target.join("config.json").exists() 为真时)每次启动都会调用 snapshot_default_dir(&default_dir, &pointer_target);而该函数内的判据是 if !dir_is_empty(default_dir) { snapshot_old_dir(default_dir) }dir_is_empty 的实现是 read_dir().next().is_none()

问题在于:snapshot_default_dir 自己在结尾会把指针文件写回 default_dir,所以从第二次启动起,default_dir 里恒有 data-home.txtdir_is_empty 恒为 false → 每次启动都 rename 出一个新的 <default>.old-<ts>(内容只有一个指针文件),然后重建目录、再写回指针。

即:启动 N 次 = 累积 N 个快照目录,永远不会被清理(list_snapshots#[allow(dead_code)],无调用方,也没有任何按时间/数量回收的逻辑,代码注释与 docs/ISSUE43_TASK_TREE.md 声称的「30 天窗口」并未实现)。

最小修复:把判据从「目录非空」改成「目录内含指针文件之外的条目」,例如用 list_snapshots/显式忽略 POINTER_FILE 后再判断;或在「已迁移」分支只做 create_dir_all 而不调 snapshot_default_dir(真正需要快照的时机只有「本次刚完成迁移」那一次)。

非阻塞建议

  1. snapshot_old_dir 的时间戳是秒级(%Y%m%d%H%M%S):同一秒内两次快照会因 rename 目标已存在而静默失败(best-effort),建议加毫秒或后缀去重。
  2. 迁移是启动期同步阻塞的(10 分钟量级的大目录),期间主窗口已创建但 AppState 尚未 manage,UI 会处于不可交互/无状态状态。建议在长期方案里给迁移加进度反馈;至少把这点的耗时预期写进 restartHint 文案。
  3. copy_tree 遇到符号链接/junction 时会解引用并递归复制目标内容:本机真实数据目录里存在 versions/*homes/*/profiles/* 目录 symlink 及 pnpm 的 node_modules junction(同一份内容被 store 与 profile 双向引用),迁移后会变成真实副本 → 目标目录体积显著放大、耗时变长。当前实现「安全但昂贵」,建议在文档/提示里明确,或后续按「保留链接」优化。
  4. DSH_LAUNCHER_DATA_HOME 指向的目录不可用时回退默认目录 + notice 的行为已实现,建议补一条单测(bootstrap 目前无测试,env/指针/回退三条分支都未覆盖)。
  5. migrate::bootstrap 的细节(半成品标记清理、指针指向自身、相对路径 env 值)建议补齐单测;remove_tree(&pointer_target) 在「中断恢复」路径上会整目录删除用户目标目录,建议至少确认目标目录内确实带有 MIGRATION_IN_FLAG 标记后再删(当前先判 MIGRATION_FLAG.exists(),但标记文件与用户既有数据可能共存于同一目录——commit_data_dir 已要求目标为空目录,所以风险有限,仅作提示)。

验证情况

  • 已完整阅读 migrate.rs(解析链、状态机、copy_tree、校验、回滚、快照)、其 7 个单测、lib.rs 的接线与 AppState 字段、commands.rs/config.rs 的路径消费点、Settings.vue 交互、api/index.ts + api/types.ts + 双语 i18n 与 mock 三件套同步情况。
  • 已确认无任何迁移后路径重写逻辑(git grep 全仓 homes/versions 路径赋值与 load_config 的启动态函数清单)。
  • CI 4/4 通过;未复现本地构建。

修好两个阻塞项后即可合入——这个 PR 的价值很高(issue #43 是真实痛点),主要是迁移后的路径一致性这一环。

- 迁移复制完成后将 config.json 中落在旧目录下的 versions[].dir /
  homes[].path 重写到新目录;已迁移分支每次启动幂等自愈
- 默认目录只剩指针文件时不再快照,修复每次启动累积 .old-* 目录
- 补 30 天快照保留期的实际裁剪(prune_old_snapshots)
- 新增 7 个测试:路径重写/幂等/坏配置容错/指针忽略/快照行为
@Gu-ZT
Gu-ZT merged commit dfcd5e3 into dsh-plugins:main Sep 14, 2026
8 checks passed
@Gu-ZT

Gu-ZT commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

审查后修复并合入(dfcd5e3)。

合入前由我修复的阻断问题:

  1. config 绝对路径未重写versions[].dir / homes[].path 为绝对路径,迁移后仍指向被改名走旧目录,所有已有实例/版本引用全断。现在 migrate_data_dir 复制完成后重写 config.json 中落在旧目录下的路径;已迁移分支每次启动幂等自愈(覆盖用旧版本迁移过的用户)。
  2. 每启动快照累积:迁移后默认目录只剩指针文件,但 dir_is_empty 判定为非空,导致每次启动都生成一个只含指针文件的 .old-* 快照且永不清理。现在快照守卫忽略指针文件(dir_has_payload),并补上文档承诺的 30 天保留期裁剪(prune_old_snapshots)。

新增 7 个测试(路径重写/幂等/坏配置容错/指针忽略/快照行为/保留期裁剪)。本地门禁:fmt ✓ / clippy -D warnings ✓ / cargo test 126 ✓;CI Quality 三平台 ✓。

遗留(非阻断,后续 issue 跟进):copy_tree 解引用 junction 导致迁移慢且体积膨胀(实测 15400 个 reparse point / 11284 份冗余拷贝,但无循环,安全)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants