* 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>
127 lines
5.3 KiB
Plaintext
127 lines
5.3 KiB
Plaintext
{ 升级换目录时,旧配置会迁移到新目录。任一目录已有配置,就跳过安装选项。}
|
||
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;
|