fix(installer): 升级不再删除 custom_translations.txt (#1035)

* 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>
This commit is contained in:
Ten-Shineandfanlusky authored and GitHub committed 2026-10-17 21:46:48 +08:00
1 parent a0da400ae0
commit 4eed9c8442
6 files changed
+60 -17

No files matched your search

+1 -1
View File
@@ -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
+1 -1
View File
@@ -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`
+38 -11
View File
@@ -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;
+16 -2
View File
@@ -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;
+3 -2
View File
@@ -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,迁移清单按文件解析取不到它们。
+1
View File
@@ -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"))