Files
Ten-Shineandfanlusky 4eed9c8442 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>
2026-10-17 21:46:48 +08:00

127 lines
5.3 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{ 升级换目录时,旧配置会迁移到新目录。任一目录已有配置,就跳过安装选项。}
function UserConfigExistsBeforeInstall: Boolean;
begin
Result := FileExists(UserConfigPath);
if (not Result) and
(CompareText(GetDataDir(''), ResolvePreviousDataDir) <> 0) then
Result := FileExists(AddBackslash(ResolvePreviousDataDir) + 'config.toml');
end;
function SameFileContent(const A, B: String): Boolean;
begin
try
Result := CompareText(GetSHA256OfFile(A), GetSHA256OfFile(B)) = 0;
except
Result := False;
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;
begin
Result := '';
if (OldDir = '') or (CompareText(OldDir, NewDir) = 0) or
(not DirExists(OldDir)) then
Exit;
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
DropMigratedCopy(OldDir, NewDir, 'config.toml');
DropMigratedCopy(OldDir, NewDir, 'custom_translations.txt');
end;
{ 网络页因为旧配置存在而跳过。旧配置没搬过去,新目录就会装上出厂配置,
用户关掉的云候选会被悄悄打开,所以必须中止安装。}
function ConfigMigrationFailure(const OldDir, NewDir: String): String;
begin
Result := '';
if FileExists(AddBackslash(OldDir) + 'config.toml') and
(not FileExists(AddBackslash(NewDir) + 'config.toml')) then
Result :=
'无法把用户配置从 ' + OldDir + ' 移动到 ' + NewDir + '。' + #13#10 +
'请确认输入法相关进程已全部退出、目标磁盘可写,然后重试安装。' + #13#10 +
'你的配置仍留在 ' + 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;
var
Lines: TArrayOfString;
Index: Integer;
Trimmed: String;
InGeneral: Boolean;
Problem: String;
begin
if UserConfigExistedBeforeInstall or (NetworkPage = nil) then
Exit;
if NetworkPage.Values[CloudCandidatesIndex] then
Exit;
Problem := '缺少 cloud_candidates';
if not LoadStringsFromFile(UserConfigPath, Lines) then
Problem := '无法读取'
else
begin
InGeneral := False;
for Index := 0 to GetArrayLength(Lines) - 1 do
begin
Trimmed := Trim(Lines[Index]);
if (Length(Trimmed) > 0) and (Trimmed[1] = '[') then
InGeneral := (Trimmed = '[general]')
else if InGeneral and (Pos('cloud_candidates', Trimmed) = 1) then
begin
Lines[Index] := 'cloud_candidates = false';
if SaveStringsToUTF8FileWithoutBOM(UserConfigPath, Lines, False) then
Exit;
Problem := '无法保存';
Break;
end;
end;
end;
Log('Cannot apply cloud candidates choice to ' + UserConfigPath + ': ' + Problem);
SuppressibleMsgBox(
'无法把「不启用云候选」写入 ' + UserConfigPath + '(' + Problem + ')。' + #13#10 +
'云候选目前仍是开启状态,请在安装完成后到「设置 → 输入」里关闭它。',
mbError, MB_OK, IDOK);
end;