From 4eed9c8442a0de99ea5bde6c4922305f2a3dba64 Mon Sep 17 00:00:00 2001 From: Ten-Shine <76210622+Ten-Shine@users.noreply.github.com> Date: Sat, 17 Oct 2026 21:46:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(installer):=20=E5=8D=87=E7=BA=A7=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E5=88=A0=E9=99=A4=20custom=5Ftranslations.txt=20(#103?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(installer): 升级不再删除 custom_translations.txt custom_translations.txt 是用户手工放在数据目录根下的中英翻译覆盖表, 安装包不携带、引擎只读不写。覆盖安装时 CleanAppDataExceptUserFiles 会 清掉根下所有不在 IsPreservedAppDataItem 名单里的条目,换数据目录时 MigrateUserDataDir 的 robocopy 清单也不含它,于是升级会把它删掉,或留在 旧目录随 RemoveDataDir 一起消失。它没有 config.toml 那样的出厂副本可 回退,删掉即内容彻底丢失。 新增 IsUserTranslationFile 并接入 IsPreservedAppDataItem,覆盖安装时与 其他用户数据同等保留;MigrateUserDataDir 的 robocopy 文件清单补上它, 换数据目录时一并搬走。preserve-user-data.ps1 的 $required 同步更新。 同步 installer/README.md 的迁移清单与 PRIVACY.md 的升级保留说明,并订正 新增函数注释里引用的 README 章节名。 * fix(installer): 迁移 custom_translations.txt 时不覆盖、不静默丢失 换数据目录时 robocopy /MOVE 会用旧目录那份直接盖掉目标目录里已有的 custom_translations.txt,而冲突检查只管 config.toml。现在 ConfigMigrationProblem 对它做同样的检查:两份内容不同就拒绝这个目录,相同就删掉旧目录的残留再搬。 这也避免了 RemoveDataDir 在非独占目录里按名字删掉没搬走的那一份。 迁移失败时原先只有用户词库和 config.toml 会中止安装,custom_translations.txt 被当成皮肤一样可重建的东西只记日志。它没有出厂副本,新增 TranslationMigrationFailure,没搬成同样中止。 同步 PRIVACY.md 与 installer/README.md 的保留/迁移清单,补上此前就漏掉的 stats.db、helpcodes\custom、shuangpin、models。 --------- Co-authored-by: fanlusky <1730976608@qq.com> --- PRIVACY.md | 2 +- installer/README.md | 2 +- installer/cloud_choice.iss | 49 ++++++++++++++++++++------ installer/msime_setup.iss | 18 ++++++++-- installer/tests/preserve-user-data.ps1 | 5 +-- installer/tests/test_cloud_choice.py | 1 + 6 files changed, 60 insertions(+), 17 deletions(-) diff --git a/PRIVACY.md b/PRIVACY.md index aeb7f82b..137ac778 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -40,7 +40,7 @@ All of these files sit in the current user's profile and are protected only by t ## Uninstall -Uninstalling deletes `%LOCALAPPDATA%\metasequoiaime` and `%PROGRAMDATA%\metasequoiaime` in full, including the user dictionary, settings and any installed skins. Back up `msime_user.db` first if you want to keep learned words. An in-place upgrade behaves differently: it preserves `msime_user.db`, `config.toml`, `config.base.toml` and the `skins` directory, and replaces everything else. +Uninstalling deletes `%LOCALAPPDATA%\metasequoiaime` and `%PROGRAMDATA%\metasequoiaime` in full, including the user dictionary, settings and any installed skins. Back up `msime_user.db` first if you want to keep learned words. An in-place upgrade behaves differently: it preserves `msime_user.db`, `stats.db`, `config.toml`, `config.base.toml`, `custom_translations.txt`, and the `skins`, `shuangpin`, `models` and `helpcodes\custom` directories, and replaces everything else. ## General diff --git a/installer/README.md b/installer/README.md index 73cfb2d5..34bd6483 100644 --- a/installer/README.md +++ b/installer/README.md @@ -38,7 +38,7 @@ pwsh -File ./Prepare-PackageFiles.ps1 -TargetVersion 1.2.3 -RepoRoot .. ` - 默认值 `%LOCALAPPDATA%\metasequoiaime`;升级安装时默认沿用上次的位置 - 选择写进 `HKLM\Software\Metasequoia\MetasequoiaIME` 的 `DataDir`。这是运行期唯一的权威来源:Server(`server/src/utils/ime_paths.cpp`)、TSF DLL(`windows/src/Utils/FanyUtils.cpp`)和引擎(`engine/core/data_path.h`)各自按 `METASEQUOIA_IME_DATA_DIR` 环境变量 → 该注册表值 → `%LOCALAPPDATA%\metasequoiaime` 的顺序解析,三处必须保持一致。32 位 TSF DLL 用 `KEY_WOW64_64KEY` 读,所以这个值必须写在 64 位视图里 -- 升级时改了位置,安装器会把用户词库(`msime_user.db` 及其 WAL/SHM)、`config.toml`、`config.base.toml` 和 `skins\` 搬到新目录,再删掉旧目录;词库和设置窗口的图片资源由本次安装重新写入,不搬 +- 升级时改了位置,安装器会把用户词库(`msime_user.db` 及其 WAL/SHM)、输入统计库(`stats.db` 及其 WAL/SHM)、`config.toml`、`config.base.toml`、`custom_translations.txt`,以及 `skins\`、`helpcodes\custom\`、`shuangpin\`、`models\` 搬到新目录,再删掉旧目录。`config.toml` 或 `custom_translations.txt` 在新旧目录里各有一份且内容不同时,安装器拒绝这个目录,不覆盖任何一份;`custom_translations.txt` 没搬成会中止安装;词库和设置窗口的图片资源由本次安装重新写入,不搬 - **数据目录必须整个归输入法**([#537](https://github.com/metasequoiaime/MSIME-Windows/issues/537))。用户选的目录里已经有别的文件、或者选了驱动器根目录时,安装器改用其中的 `metasequoiaime` 子目录,并在向导里把实际位置告诉用户;连那个子目录也不空就拒绝。输入法用过的目录(有标记,或历史默认位置)原样沿用 - 安装器在数据目录里放一个 `.metasequoiaime-data` 标记文件。只有标记里带 `exclusive: adopted empty by the installer` 这一行(安装时目录是空的),或目录就是历史默认位置,覆盖安装才整目录清理、卸载才整目录删除 - 旧版安装器不管目录里原来有什么都写标记,所以只有旧标记(没有那一行)的目录不能再信:覆盖安装、换目录和卸载都只按 `IsShippedAppDataItem` / `IsRuntimeAppDataItem` / `IsPreservedAppDataItem` 名单删输入法自己的条目,`assets`、`logs`、`skins` 这种通用目录名(以及已退役的 `html`)只删输入法写进去的部分,最后目录空了才删目录。包里新增顶层文件要同步 `IsShippedAppDataItem`,`tests/package-files.ps1` 会拿真实打出的 `app_data` 核对;Server 在数据目录根下新写文件要同步 `IsRuntimeAppDataItem` diff --git a/installer/cloud_choice.iss b/installer/cloud_choice.iss index 5dd69b58..609d3939 100644 --- a/installer/cloud_choice.iss +++ b/installer/cloud_choice.iss @@ -16,29 +16,44 @@ begin end; end; +function MigratedFileConflicts(const OldDir, NewDir, FileName: String): Boolean; +begin + Result := + FileExists(AddBackslash(OldDir) + FileName) and + FileExists(AddBackslash(NewDir) + FileName) and + (not SameFileContent(AddBackslash(OldDir) + FileName, AddBackslash(NewDir) + FileName)); +end; + { 迁移前先检查冲突。迁移时再次检查,防止目录内容在停机期间改变。 - 两份内容相同说明上次迁移已经复制过去、只是没删掉源文件,重试时不算冲突。} + 两份内容相同说明上次迁移已经复制过去、只是没删掉源文件,重试时不算冲突。 + custom_translations.txt 同样要查:它是用户手写的翻译覆盖表,没有出厂副本, + robocopy /MOVE 会直接用旧目录那份盖掉目标目录那份。} function ConfigMigrationProblem(const OldDir, NewDir: String): String; -var - OldConfig, NewConfig: String; begin Result := ''; if (OldDir = '') or (CompareText(OldDir, NewDir) = 0) or (not DirExists(OldDir)) then Exit; - OldConfig := AddBackslash(OldDir) + 'config.toml'; - NewConfig := AddBackslash(NewDir) + 'config.toml'; - if FileExists(OldConfig) and FileExists(NewConfig) and - (not SameFileContent(OldConfig, NewConfig)) then - Result := '目标数据目录已有 config.toml,不能覆盖原有配置。请选择其他数据目录。'; + if MigratedFileConflicts(OldDir, NewDir, 'config.toml') then + Result := '目标数据目录已有 config.toml,不能覆盖原有配置。请选择其他数据目录。' + else if MigratedFileConflicts(OldDir, NewDir, 'custom_translations.txt') then + Result := + '目标数据目录已有内容不同的 custom_translations.txt,不能覆盖原有的自定义翻译。' + #13#10 + + '请选择其他数据目录,或先手动合并两份文件。'; end; { 冲突检查通过后两份还同时存在,只能是上次迁移留下的相同副本,删掉源文件再搬。} +procedure DropMigratedCopy(const OldDir, NewDir, FileName: String); +begin + if FileExists(AddBackslash(OldDir) + FileName) and + FileExists(AddBackslash(NewDir) + FileName) then + DeleteFile(AddBackslash(OldDir) + FileName); +end; + procedure DropMigratedConfigCopy(const OldDir, NewDir: String); begin - if FileExists(AddBackslash(OldDir) + 'config.toml') and - FileExists(AddBackslash(NewDir) + 'config.toml') then - DeleteFile(AddBackslash(OldDir) + 'config.toml'); + DropMigratedCopy(OldDir, NewDir, 'config.toml'); + DropMigratedCopy(OldDir, NewDir, 'custom_translations.txt'); end; { 网络页因为旧配置存在而跳过。旧配置没搬过去,新目录就会装上出厂配置, @@ -54,6 +69,18 @@ begin '你的配置仍留在 ' + OldDir + ',本次安装没有改动它。'; end; +{ 自定义翻译没搬过去,新目录里就没有覆盖表,用户只会看到翻译回退成大表释义,不会有任何报错。} +function TranslationMigrationFailure(const OldDir, NewDir: String): String; +begin + Result := ''; + if FileExists(AddBackslash(OldDir) + 'custom_translations.txt') and + (not FileExists(AddBackslash(NewDir) + 'custom_translations.txt')) then + Result := + '无法把自定义翻译 custom_translations.txt 从 ' + OldDir + ' 移动到 ' + NewDir + '。' + #13#10 + + '请确认文件没有被其他程序占用、目标磁盘可写,然后重试安装。' + #13#10 + + '该文件仍留在 ' + OldDir + ',本次安装没有改动它。'; +end; + { 只改本次安装新生成的 config.toml。写入失败不中止安装:此时文件已经落盘, 中止只会留下一个缺登录任务和目录权限的半成品。改为提示用户到设置页手动关闭。} procedure ApplyNetworkChoiceToUserConfig; diff --git a/installer/msime_setup.iss b/installer/msime_setup.iss index 67e3c10d..fdd25f39 100644 --- a/installer/msime_setup.iss +++ b/installer/msime_setup.iss @@ -781,6 +781,14 @@ begin (CompareText(FileName, 'config.base.toml') = 0); end; +function IsUserTranslationFile(const FileName: String): Boolean; +begin + { 候选窗中英翻译的人工覆盖表,由用户手工放在数据目录根下(README「自定义候选窗翻译」一节), + 安装包不携带、引擎只读不写。它没有 config.toml 那样的出厂副本可回退,删掉就是内容 + 彻底丢失,所以必须与用户配置同等对待。} + Result := CompareText(FileName, 'custom_translations.txt') = 0; +end; + function IsUserSkinDirectory(const FileName: String): Boolean; begin { 外部皮肤在 %LOCALAPPDATA%\metasequoiaime\skins,升级安装不得清掉。} @@ -812,6 +820,7 @@ begin IsUserDatabaseFile(FileName) or IsUserStatisticsDatabase(FileName) or IsUserConfigFile(FileName) or + IsUserTranslationFile(FileName) or IsUserSkinDirectory(FileName) or IsUserShuangpinDirectory(FileName) or IsCollocationModelDirectory(FileName) or @@ -1370,7 +1379,7 @@ begin '"' + RemoveBackslashUnlessRoot(OldDir) + '" "' + RemoveBackslashUnlessRoot(NewDir) + '" ' + 'msime_user.db msime_user.db-wal msime_user.db-shm msime_user.db-journal ' + 'stats.db stats.db-wal stats.db-shm stats.db-journal ' + - 'config.toml config.base.toml /MOVE /R:2 /W:1 /NJH /NJS /NP /NFL /NDL', + 'config.toml config.base.toml custom_translations.txt /MOVE /R:2 /W:1 /NJH /NJS /NP /NFL /NDL', '', SW_HIDE, ewWaitUntilTerminated, @@ -1435,14 +1444,19 @@ begin { 用户词库没搬成必须让安装失败。否则新版本会对着一个空的 msime_user.db 启动, ssPostInstall 的回放只会记一句 skipped,用户的自造词和词频就这么静悄悄没了—— 这正是 ReplayUserDictionary 失败时也要中止安装的那条理由。 - config.toml 同理,见 ConfigMigrationFailure。皮肤等搬不动只记日志:它们丢了可以重建。} + config.toml 同理,见 ConfigMigrationFailure;custom_translations.txt 没有出厂副本, + 见 TranslationMigrationFailure。皮肤等搬不动只记日志:它们丢了可以重建。} if FileExists(AddBackslash(OldDir) + 'msime_user.db') then Result := '无法把用户词库从 ' + OldDir + ' 移动到 ' + NewDir + '。' + #13#10 + '请确认输入法相关进程已全部退出、目标磁盘可写且空间足够,然后重试安装。' + #13#10 + '你的用户词库仍留在 ' + OldDir + ',本次安装没有改动它。' else + begin Result := ConfigMigrationFailure(OldDir, NewDir); + if Result = '' then + Result := TranslationMigrationFailure(OldDir, NewDir); + end; exit; end; diff --git a/installer/tests/preserve-user-data.ps1 b/installer/tests/preserve-user-data.ps1 index a35180aa..f7c78346 100644 --- a/installer/tests/preserve-user-data.ps1 +++ b/installer/tests/preserve-user-data.ps1 @@ -13,11 +13,12 @@ $issPath = Join-Path $PSScriptRoot '../msime_setup.iss' $iss = [IO.File]::ReadAllText((Resolve-Path $issPath)) # 必须活过覆盖安装的用户数据。主库 + 三个 sidecar:安装器会强杀 Server,WAL 里可能还有尚未 -# checkpoint 的写入,只搬主库会丢掉最后一批。 +# checkpoint 的写入,只搬主库会丢掉最后一批。custom_translations.txt 没有出厂副本可回退, +# 漏掉它同样是内容彻底丢失。 $required = @( 'msime_user.db', 'msime_user.db-wal', 'msime_user.db-shm', 'msime_user.db-journal', 'stats.db', 'stats.db-wal', 'stats.db-shm', 'stats.db-journal', - 'config.toml', 'config.base.toml' + 'config.toml', 'config.base.toml', 'custom_translations.txt' ) # 目录类用户数据由 MigrateUserDataDir 单独 robocopy,迁移清单按文件解析取不到它们。 diff --git a/installer/tests/test_cloud_choice.py b/installer/tests/test_cloud_choice.py index dee58663..161a7d84 100644 --- a/installer/tests/test_cloud_choice.py +++ b/installer/tests/test_cloud_choice.py @@ -20,6 +20,7 @@ class CloudChoiceTests(unittest.TestCase): self.assertLess(prepare.index("ConfigMigrationProblem("), prepare.index("StopImeProcesses;")) self.assertLess(migrate.index("ConfigMigrationProblem("), migrate.index("Log('Migrating user data")) self.assertIn("ConfigMigrationFailure(", migrate) + self.assertIn("TranslationMigrationFailure(", migrate) def test_conflict_is_rejected_on_the_data_dir_page(self): self.assertIn("ConfigMigrationProblem(", function_body("NextButtonClick"))