diff --git a/internal/app/methods_update.go b/internal/app/methods_update.go index 5e013ea..ce812e9 100644 --- a/internal/app/methods_update.go +++ b/internal/app/methods_update.go @@ -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 diff --git a/internal/app/methods_update_test.go b/internal/app/methods_update_test.go index 894c6e5..f95a9ba 100644 --- a/internal/app/methods_update_test.go +++ b/internal/app/methods_update_test.go @@ -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")