🐛 fix(update): 修复Windows在线更新复用旧安装包

This commit is contained in:
Syngnat
2026-07-06 17:40:45 +08:00
parent 22697baa37
commit 2508b7935a
2 changed files with 213 additions and 57 deletions

View File

@@ -99,6 +99,12 @@ type stagedUpdate struct {
InstallLogPath string
}
type updatePathCandidate struct {
workspaceDir string
stagedDir string
assetPath string
}
type githubRelease struct {
TagName string `json:"tag_name"`
Name string `json:"name"`
@@ -1026,6 +1032,88 @@ func resolveUpdateStagedDirForPlatform(goos string, workspaceDir string, channel
return filepath.Join(baseDir, buildUpdateStageDirNameForPlatform(goos, channel, version))
}
func shouldReuseUpdateAssetFromStagedDirForPlatform(goos string, assetName string) bool {
return !(strings.EqualFold(strings.TrimSpace(goos), "windows") &&
shouldWindowsUpdateLaunchDownloadedAssetDirectly(assetName))
}
func normalizeUpdatePathForPrefixCheck(path string) string {
normalized := strings.ReplaceAll(strings.TrimSpace(path), "\\", "/")
normalized = filepath.ToSlash(filepath.Clean(normalized))
if normalized == "." {
return ""
}
return strings.TrimRight(normalized, "/")
}
func isUpdateAssetPathInsideStagedDir(filePath string, stagedDir string) bool {
normalizedFilePath := normalizeUpdatePathForPrefixCheck(filePath)
normalizedStagedDir := normalizeUpdatePathForPrefixCheck(stagedDir)
if normalizedFilePath == "" || normalizedStagedDir == "" {
return false
}
return normalizedFilePath == normalizedStagedDir || strings.HasPrefix(normalizedFilePath, normalizedStagedDir+"/")
}
func buildReusableUpdatePathCandidatesForPlatform(goos string, preferredWorkspaceDir string, legacyWorkspaceDir string, channel string, version string, assetName string) []updatePathCandidate {
preferredWorkspaceDir = strings.TrimSpace(preferredWorkspaceDir)
legacyWorkspaceDir = strings.TrimSpace(legacyWorkspaceDir)
assetName = strings.TrimSpace(assetName)
preferredStagedDir := resolveUpdateStagedDirForPlatform(goos, preferredWorkspaceDir, channel, version)
stagedDirNames := []string{
buildUpdateStageDirNameForPlatform(goos, channel, version),
fmt.Sprintf(".gonavi-update-%s-%s", strings.TrimSpace(strings.ToLower(goos)), version),
}
workspaceCandidates := []string{preferredWorkspaceDir, legacyWorkspaceDir}
stageBaseCandidates := []string{preferredWorkspaceDir, legacyWorkspaceDir}
seenWorkspace := make(map[string]struct{}, len(workspaceCandidates))
seenStageBase := make(map[string]struct{}, len(stageBaseCandidates))
candidates := make([]updatePathCandidate, 0, 8)
for _, workspaceDir := range workspaceCandidates {
workspaceDir = strings.TrimSpace(workspaceDir)
if workspaceDir == "" {
continue
}
if _, exists := seenWorkspace[workspaceDir]; exists {
continue
}
seenWorkspace[workspaceDir] = struct{}{}
if shouldStoreUpdateAssetInWorkspaceRoot(goos) {
candidates = append(candidates, updatePathCandidate{
workspaceDir: workspaceDir,
stagedDir: preferredStagedDir,
assetPath: filepath.Join(workspaceDir, assetName),
})
}
}
if !shouldReuseUpdateAssetFromStagedDirForPlatform(goos, assetName) {
return candidates
}
for _, stageBaseDir := range stageBaseCandidates {
stageBaseDir = strings.TrimSpace(stageBaseDir)
if stageBaseDir == "" {
continue
}
if _, exists := seenStageBase[stageBaseDir]; exists {
continue
}
seenStageBase[stageBaseDir] = struct{}{}
for _, stagedDirName := range stagedDirNames {
stagedDir := filepath.Join(stageBaseDir, stagedDirName)
candidates = append(candidates, updatePathCandidate{
workspaceDir: stageBaseDir,
stagedDir: stagedDir,
assetPath: filepath.Join(stagedDir, assetName),
})
}
}
return candidates
}
func isExistingDownloadedAsset(filePath string, expectedSize int64) bool {
path := strings.TrimSpace(filePath)
if path == "" {
@@ -1042,6 +1130,16 @@ func isExistingDownloadedAsset(filePath string, expectedSize int64) bool {
}
func resolveReusableStagedUpdate(info UpdateInfo, current *stagedUpdate) *stagedUpdate {
return resolveReusableStagedUpdateForPlatform(
stdRuntime.GOOS,
resolveUpdateWorkspaceDir(strings.TrimSpace(info.LatestVersion)),
resolveLegacyUpdateWorkspaceDir(),
info,
current,
)
}
func resolveReusableStagedUpdateForPlatform(goos string, preferredWorkspaceDir string, legacyWorkspaceDir string, info UpdateInfo, current *stagedUpdate) *stagedUpdate {
channel, err := normalizeUpdateChannel(info.Channel)
if err != nil {
channel = updateChannelLatest
@@ -1051,6 +1149,7 @@ func resolveReusableStagedUpdate(info UpdateInfo, current *stagedUpdate) *staged
if version == "" || assetName == "" {
return nil
}
allowStagedDirReuse := shouldReuseUpdateAssetFromStagedDirForPlatform(goos, assetName)
if current != nil {
currentChannel := current.Channel
@@ -1060,68 +1159,27 @@ func resolveReusableStagedUpdate(info UpdateInfo, current *stagedUpdate) *staged
if currentChannel == channel && strings.TrimSpace(current.Version) == version {
currentPath := strings.TrimSpace(current.FilePath)
if isExistingDownloadedAsset(currentPath, info.AssetSize) {
if strings.TrimSpace(current.InstallLogPath) == "" {
current.InstallLogPath = buildUpdateInstallLogPath(filepath.Dir(currentPath))
if !allowStagedDirReuse && isUpdateAssetPathInsideStagedDir(currentPath, current.StagedDir) {
current = nil
} else {
if strings.TrimSpace(current.InstallLogPath) == "" {
current.InstallLogPath = buildUpdateInstallLogPath(filepath.Dir(currentPath))
}
current.Channel = channel
return current
}
current.Channel = channel
return current
}
}
}
type pathCandidate struct {
workspaceDir string
stagedDir string
assetPath string
}
preferredWorkspaceDir := resolveUpdateWorkspaceDir(version)
preferredStagedDir := resolveUpdateStagedDir(preferredWorkspaceDir, string(channel), version)
legacyWorkspaceDir := resolveLegacyUpdateWorkspaceDir()
stagedDirNames := []string{
buildUpdateStageDirName(string(channel), version),
fmt.Sprintf(".gonavi-update-%s-%s", stdRuntime.GOOS, version),
}
workspaceCandidates := []string{preferredWorkspaceDir, legacyWorkspaceDir}
stageBaseCandidates := []string{preferredWorkspaceDir, legacyWorkspaceDir}
seenWorkspace := make(map[string]struct{}, len(workspaceCandidates))
seenStageBase := make(map[string]struct{}, len(stageBaseCandidates))
candidates := make([]pathCandidate, 0, 8)
for _, workspaceDir := range workspaceCandidates {
workspaceDir = strings.TrimSpace(workspaceDir)
if workspaceDir == "" {
continue
}
if _, exists := seenWorkspace[workspaceDir]; exists {
continue
}
seenWorkspace[workspaceDir] = struct{}{}
if shouldStoreUpdateAssetInWorkspaceRoot(stdRuntime.GOOS) {
candidates = append(candidates, pathCandidate{
workspaceDir: workspaceDir,
stagedDir: preferredStagedDir,
assetPath: filepath.Join(workspaceDir, assetName),
})
}
}
for _, stageBaseDir := range stageBaseCandidates {
stageBaseDir = strings.TrimSpace(stageBaseDir)
if stageBaseDir == "" {
continue
}
if _, exists := seenStageBase[stageBaseDir]; exists {
continue
}
seenStageBase[stageBaseDir] = struct{}{}
for _, stagedDirName := range stagedDirNames {
stagedDir := filepath.Join(stageBaseDir, stagedDirName)
candidates = append(candidates, pathCandidate{
workspaceDir: stageBaseDir,
stagedDir: stagedDir,
assetPath: filepath.Join(stagedDir, assetName),
})
}
}
candidates := buildReusableUpdatePathCandidatesForPlatform(
goos,
preferredWorkspaceDir,
legacyWorkspaceDir,
string(channel),
version,
assetName,
)
for _, candidate := range candidates {
if !isExistingDownloadedAsset(candidate.assetPath, info.AssetSize) {
continue

View File

@@ -327,6 +327,104 @@ func TestResolveReusableStagedUpdateDoesNotReuseDifferentChannelPackage(t *testi
}
}
func TestResolveReusableStagedUpdateForPlatformSkipsLegacyWindowsExeStagedAsset(t *testing.T) {
preferredWorkspaceDir := t.TempDir()
legacyWorkspaceDir := t.TempDir()
info := UpdateInfo{
Channel: string(updateChannelLatest),
LatestVersion: "0.8.4",
AssetName: "GoNavi-0.8.4-Windows-Amd64.exe",
AssetSize: 8,
}
legacyStagedDir := filepath.Join(
legacyWorkspaceDir,
buildUpdateStageDirNameForPlatform("windows", info.Channel, info.LatestVersion),
)
if err := os.MkdirAll(legacyStagedDir, 0o755); err != nil {
t.Fatalf("MkdirAll returned error: %v", err)
}
legacyAssetPath := filepath.Join(legacyStagedDir, info.AssetName)
if err := os.WriteFile(legacyAssetPath, []byte("12345678"), 0o644); err != nil {
t.Fatalf("WriteFile returned error: %v", err)
}
reused := resolveReusableStagedUpdateForPlatform("windows", preferredWorkspaceDir, legacyWorkspaceDir, info, nil)
if reused != nil {
t.Fatalf("expected legacy staged windows exe to be ignored, got %#v", reused)
}
}
func TestResolveReusableStagedUpdateForPlatformPrefersWindowsExeInInstallDirectory(t *testing.T) {
preferredWorkspaceDir := t.TempDir()
legacyWorkspaceDir := t.TempDir()
info := UpdateInfo{
Channel: string(updateChannelLatest),
LatestVersion: "0.8.4",
AssetName: "GoNavi-0.8.4-Windows-Amd64.exe",
AssetSize: 8,
}
preferredAssetPath := filepath.Join(preferredWorkspaceDir, info.AssetName)
if err := os.WriteFile(preferredAssetPath, []byte("12345678"), 0o644); err != nil {
t.Fatalf("WriteFile returned error: %v", err)
}
legacyStagedDir := filepath.Join(
legacyWorkspaceDir,
buildUpdateStageDirNameForPlatform("windows", info.Channel, info.LatestVersion),
)
if err := os.MkdirAll(legacyStagedDir, 0o755); err != nil {
t.Fatalf("MkdirAll returned error: %v", err)
}
legacyAssetPath := filepath.Join(legacyStagedDir, info.AssetName)
if err := os.WriteFile(legacyAssetPath, []byte("87654321"), 0o644); err != nil {
t.Fatalf("WriteFile returned error: %v", err)
}
reused := resolveReusableStagedUpdateForPlatform("windows", preferredWorkspaceDir, legacyWorkspaceDir, info, nil)
if reused == nil {
t.Fatal("expected install-directory windows exe to be reused")
}
if reused.FilePath != preferredAssetPath {
t.Fatalf("expected preferred install-directory asset %q, got %q", preferredAssetPath, reused.FilePath)
}
}
func TestResolveReusableStagedUpdateForPlatformDoesNotReuseCurrentWindowsExeInsideStagedDir(t *testing.T) {
preferredWorkspaceDir := t.TempDir()
legacyWorkspaceDir := t.TempDir()
info := UpdateInfo{
Channel: string(updateChannelLatest),
LatestVersion: "0.8.4",
AssetName: "GoNavi-0.8.4-Windows-Amd64.exe",
AssetSize: 8,
}
legacyStagedDir := filepath.Join(
legacyWorkspaceDir,
buildUpdateStageDirNameForPlatform("windows", info.Channel, info.LatestVersion),
)
if err := os.MkdirAll(legacyStagedDir, 0o755); err != nil {
t.Fatalf("MkdirAll returned error: %v", err)
}
legacyAssetPath := filepath.Join(legacyStagedDir, info.AssetName)
if err := os.WriteFile(legacyAssetPath, []byte("12345678"), 0o644); err != nil {
t.Fatalf("WriteFile returned error: %v", err)
}
reused := resolveReusableStagedUpdateForPlatform("windows", preferredWorkspaceDir, legacyWorkspaceDir, info, &stagedUpdate{
Channel: updateChannelLatest,
Version: info.LatestVersion,
AssetName: info.AssetName,
FilePath: legacyAssetPath,
StagedDir: legacyStagedDir,
})
if reused != nil {
t.Fatalf("expected current staged windows exe inside staging dir to be ignored, got %#v", reused)
}
}
func TestDownloadUpdateUsesCurrentLanguageForBackendMessage(t *testing.T) {
app := NewApp()
app.SetLanguage("en-US")