feat(tui): 校验后原子保存 + 时间戳备份(第一轮第五块·地基)
三个配置编辑器共用的这块地基(ADR-0013)。顺序是刻意的:① 先校验(调用方给的 -Validate 回调);② **只有校验通过才备份与写盘** —— 校验没过还留下备份会让人以为"动过了";③ 原子替换走 Write-BaknretAtomicText。返回结果对象而不是抛异常:TUI 要把错误显示出来让人继续改,而不是把界面炸掉("TUI 异常不改退出码"这条同样适用)。 给 Write-BaknretAtomicText 加了一个可选 -Encoding(不传时沿用原行为,现有调用点不受影响)。原因:它原先固定用日志编码(无 BOM),而三个配置文件都是带 BOM 的 —— 丢掉 BOM 会让 5.1 把整个文件按 GBK 读,中文全成乱码。这类"编码悄悄变了"的错不会报错,只会让文件在某一个 PowerShell 版本上读出来是乱码。 判据(断言 10 条):校验不过时不写盘、不产生备份、错误原文能带回去;校验通过后新内容落盘、备份里是**原文**、且写回**保留 UTF-8 BOM**(逐字节验 EF BB BF)。 验收:test.ps1 9/9 全绿(5.1 与 7);真实清单只读冒烟 4/4。
This commit is contained in:
1 parent
54972ab341
commit
59ec5a21d4
5 files changed
+91
-2
No files matched your search
@@ -19,6 +19,7 @@
|
|||||||
CompatiblePSEditions = @('Desktop', 'Core')
|
CompatiblePSEditions = @('Desktop', 'Core')
|
||||||
|
|
||||||
FunctionsToExport = @(
|
FunctionsToExport = @(
|
||||||
|
'Save-BakNRetConfigFile',
|
||||||
'Get-BakNRetBackupListRow',
|
'Get-BakNRetBackupListRow',
|
||||||
'Set-BakNRetBackupListRow',
|
'Set-BakNRetBackupListRow',
|
||||||
'Invoke-BakNRetMenu',
|
'Invoke-BakNRetMenu',
|
||||||
|
|||||||
@@ -160,7 +160,10 @@
|
|||||||
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetBackupListRow.ps1')
|
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetBackupListRow.ps1')
|
||||||
. (Join-Path $PSScriptRoot 'Public\Set-BakNRetBackupListRow.ps1')
|
. (Join-Path $PSScriptRoot 'Public\Set-BakNRetBackupListRow.ps1')
|
||||||
|
|
||||||
|
. (Join-Path $PSScriptRoot 'Public\Save-BakNRetConfigFile.ps1')
|
||||||
|
|
||||||
Export-ModuleMember -Function @(
|
Export-ModuleMember -Function @(
|
||||||
|
'Save-BakNRetConfigFile',
|
||||||
'Get-BakNRetBackupListRow',
|
'Get-BakNRetBackupListRow',
|
||||||
'Set-BakNRetBackupListRow',
|
'Set-BakNRetBackupListRow',
|
||||||
'Invoke-BakNRetMenu',
|
'Invoke-BakNRetMenu',
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
function Save-BakNRetConfigFile {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
把新内容保存回配置文件:先校验,过了才做时间戳备份 + 原子替换;不过就原样不动并带回错误。
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
三个配置编辑器(清单 / 配置 / 名录)共用这块地基(ADR-0013)。顺序是刻意安排的:
|
||||||
|
|
||||||
|
1. **先校验**(调用方给的 -Validate 回调返回错误字符串);
|
||||||
|
2. **只有校验通过才备份与写盘** —— 校验没过还留下备份,会让人以为"动过了";
|
||||||
|
3. 原子替换走 Write-BaknretAtomicText(临时文件 + 替换,失败时原文件完好)。
|
||||||
|
|
||||||
|
返回值是结果对象而不是抛异常:TUI 需要把错误**显示出来**让人继续改,而不是把界面炸掉。
|
||||||
|
"TUI 的异常绝不改退出码"(ADR-0010)这条同样适用。
|
||||||
|
|
||||||
|
编码默认 UTF-8 **带 BOM** —— 仓库里这三个文件都是带 BOM 的(.editorconfig 与验收的 Encode
|
||||||
|
层都这么要求),丢掉 BOM 会让 5.1 把整个文件按 GBK 读,中文全成乱码。
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$Path,
|
||||||
|
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$Text,
|
||||||
|
[string]$BackupDirectory,
|
||||||
|
[scriptblock]$Validate,
|
||||||
|
[System.Text.Encoding]$Encoding,
|
||||||
|
[switch]$WhatIf
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $Encoding) { $Encoding = New-Object System.Text.UTF8Encoding($true) }
|
||||||
|
|
||||||
|
$errors = @()
|
||||||
|
if ($Validate) { $errors = @(& $Validate $Text | Where-Object { $_ }) }
|
||||||
|
if ($errors.Count -gt 0) {
|
||||||
|
return [pscustomobject]@{ Saved = $false; BackupPath = $null; Error = ($errors -join ';') }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $BackupDirectory) {
|
||||||
|
$BackupDirectory = Join-Path (Split-Path -Parent $Path) 'logs\config-backups'
|
||||||
|
}
|
||||||
|
|
||||||
|
$backupPath = $null
|
||||||
|
if ((Test-Path -LiteralPath $Path) -and -not $WhatIf) {
|
||||||
|
if (-not (Test-Path -LiteralPath $BackupDirectory)) {
|
||||||
|
New-Item -ItemType Directory -Path $BackupDirectory -Force | Out-Null
|
||||||
|
}
|
||||||
|
$stamp = (Get-Date).ToString('yyyyMMdd-HHmmss')
|
||||||
|
$backupPath = Join-Path $BackupDirectory ('{0}-{1}.bak' -f [System.IO.Path]::GetFileName($Path), $stamp)
|
||||||
|
Copy-Item -LiteralPath $Path -Destination $backupPath -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $WhatIf) {
|
||||||
|
Write-BaknretAtomicText -Path $Path -Text $Text -Encoding $Encoding | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pscustomobject]@{ Saved = $true; BackupPath = $backupPath; Error = '' }
|
||||||
|
}
|
||||||
@@ -5,7 +5,10 @@
|
|||||||
#>
|
#>
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)][string]$Path,
|
[Parameter(Mandatory = $true)][string]$Path,
|
||||||
[AllowEmptyString()][string]$Text = ''
|
[AllowEmptyString()][string]$Text = '',
|
||||||
|
# 默认沿用日志编码(无 BOM)。配置文件要传带 BOM 的 UTF8Encoding —— 丢掉 BOM 会让
|
||||||
|
# Windows PowerShell 5.1 把整个文件按 GBK 读,中文全成乱码。
|
||||||
|
[System.Text.Encoding]$Encoding
|
||||||
)
|
)
|
||||||
|
|
||||||
$directory = Split-Path -Parent $Path
|
$directory = Split-Path -Parent $Path
|
||||||
@@ -14,7 +17,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$temp = "$Path.tmp"
|
$temp = "$Path.tmp"
|
||||||
[System.IO.File]::WriteAllText($temp, $Text, $script:LogEncoding)
|
[System.IO.File]::WriteAllText($temp, $Text, $(if ($Encoding) { $Encoding } else { $script:LogEncoding }))
|
||||||
|
|
||||||
if (-not (Test-Path -LiteralPath $Path)) {
|
if (-not (Test-Path -LiteralPath $Path)) {
|
||||||
Move-BakNRetArchiveIntoPlace -TempPath $temp -DestinationPath $Path
|
Move-BakNRetArchiveIntoPlace -TempPath $temp -DestinationPath $Path
|
||||||
|
|||||||
@@ -1597,6 +1597,33 @@ Test-Case '清单行表与单行替换:只动那一行,注释与其它行逐
|
|||||||
Assert-True $threw '行号越界必须抛错'
|
Assert-True $threw '行号越界必须抛错'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Test-Case '保存配置:校验不过什么都不动,过了才备份 + 原子替换,且保留 BOM' {
|
||||||
|
$dir = Join-Path $env:TEMP ("bnr-save-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
|
||||||
|
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||||
|
$file = Join-Path $dir 'BackupList.txt'
|
||||||
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
[System.IO.File]::WriteAllText($file, "Edge`n", $utf8Bom)
|
||||||
|
try {
|
||||||
|
# 1) 校验不过:文件不动、也不产生备份(留备份会让人以为"动过了")
|
||||||
|
$bad = Save-BakNRetConfigFile -Path $file -Text "坏行`n" -Validate { param($t) @('第 1 行有问题') } -BackupDirectory (Join-Path $dir 'bak')
|
||||||
|
Assert-Equal $false $bad.Saved '校验不过不能保存'
|
||||||
|
Assert-True ($bad.Error -like '*第 1 行有问题*') '错误原文要带回来(TUI 要显示它)'
|
||||||
|
Assert-Equal "Edge`n" ([System.IO.File]::ReadAllText($file)) '文件内容不动'
|
||||||
|
Assert-True (-not (Test-Path (Join-Path $dir 'bak'))) '校验不过不该产生备份'
|
||||||
|
|
||||||
|
# 2) 校验通过:写进去 + 备份里是原文 + 保留 BOM
|
||||||
|
$ok = Save-BakNRetConfigFile -Path $file -Text "+WindowsTerminal`n" -Validate { param($t) @() } -BackupDirectory (Join-Path $dir 'bak')
|
||||||
|
Assert-Equal $true $ok.Saved '校验通过应当保存'
|
||||||
|
Assert-Equal "+WindowsTerminal`n" ([System.IO.File]::ReadAllText($file)) '新内容写进去了'
|
||||||
|
Assert-True (Test-Path $ok.BackupPath) '应当产生时间戳备份'
|
||||||
|
Assert-Equal "Edge`n" ([System.IO.File]::ReadAllText($ok.BackupPath)) '备份里是原文'
|
||||||
|
$bytes = [System.IO.File]::ReadAllBytes($file)
|
||||||
|
Assert-True ($bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) '写回必须保留 UTF-8 BOM'
|
||||||
|
} finally {
|
||||||
|
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
Remove-Item -LiteralPath $sandbox -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $sandbox -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
Remove-Item -LiteralPath $itemSandbox -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $itemSandbox -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
|||||||
Reference in new issue
Block a user