v0.23.2 bfd5eb4

apply_update.go RawHistoryFolder

1package usecase
2
3import (
4	"context"
5	"fmt"
6	"os"
7	"path/filepath"
8
9	"github.com/bnema/dumber/internal/application/port"
10	"github.com/bnema/dumber/internal/domain/entity"
11	"github.com/bnema/dumber/internal/logging"
12)
13
14const (
15	// File permission for directories.
16	updateDirPerm = 0o755
17)
18
19// ApplyUpdateInput holds the input for the apply update use case.
20type ApplyUpdateInput struct {
21	// DownloadURL is the URL to download the update from.
22	DownloadURL string
23}
24
25// ApplyUpdateOutput holds the result of the update application.
26type ApplyUpdateOutput struct {
27	// Status is the current update status.
28	Status entity.UpdateStatus
29	// Message is a human-readable status message.
30	Message string
31	// StagedPath is the path to the staged binary (if status is Ready).
32	StagedPath string
33}
34
35// ApplyUpdateUseCase downloads and stages updates for application on exit.
36type ApplyUpdateUseCase struct {
37	downloader port.UpdateDownloader
38	applier    port.UpdateApplier
39	cacheDir   string
40}
41
42// NewApplyUpdateUseCase creates a new apply update use case.
43func NewApplyUpdateUseCase(
44	downloader port.UpdateDownloader,
45	applier port.UpdateApplier,
46	cacheDir string,
47) *ApplyUpdateUseCase {
48	return &ApplyUpdateUseCase{
49		downloader: downloader,
50		applier:    applier,
51		cacheDir:   cacheDir,
52	}
53}
54
55// Execute downloads and stages the update for application on exit.
56func (uc *ApplyUpdateUseCase) Execute(ctx context.Context, input ApplyUpdateInput) (*ApplyUpdateOutput, error) {
57	log := logging.FromContext(ctx)
58
59	// Check if we can self-update.
60	if !uc.applier.CanSelfUpdate(ctx) {
61		reason := uc.applier.SelfUpdateBlockedReason(ctx)
62		var message string
63		switch reason {
64		case port.SelfUpdateBlockedFlatpak:
65			message = "Cannot auto-update: installed via Flatpak (use 'flatpak update' instead)"
66		case port.SelfUpdateBlockedPacman:
67			message = "Cannot auto-update: installed via pacman/AUR (use your package manager instead)"
68		default:
69			message = "Cannot auto-update: binary is not writable"
70		}
71		return &ApplyUpdateOutput{
72			Status:  entity.UpdateStatusFailed,
73			Message: message,
74		}, nil
75	}
76
77	// Create download directory.
78	downloadDir := filepath.Join(uc.cacheDir, "updates")
79	if err := os.MkdirAll(downloadDir, updateDirPerm); err != nil {
80		return nil, fmt.Errorf("failed to create download directory: %w", err)
81	}
82
83	log.Info().Str("url", input.DownloadURL).Msg("downloading update")
84
85	// Download the archive.
86	archivePath, err := uc.downloader.Download(ctx, input.DownloadURL, downloadDir)
87	if err != nil {
88		return &ApplyUpdateOutput{
89			Status:  entity.UpdateStatusFailed,
90			Message: fmt.Sprintf("Download failed: %v", err),
91		}, nil
92	}
93
94	log.Info().Str("archive", archivePath).Msg("extracting update")
95
96	// Extract the binary.
97	binaryPath, err := uc.downloader.Extract(ctx, archivePath, downloadDir)
98	if err != nil {
99		_ = os.Remove(archivePath)
100		return &ApplyUpdateOutput{
101			Status:  entity.UpdateStatusFailed,
102			Message: fmt.Sprintf("Extraction failed: %v", err),
103		}, nil
104	}
105
106	log.Info().Str("binary", binaryPath).Msg("staging update")
107
108	// Stage the update.
109	if err := uc.applier.StageUpdate(ctx, binaryPath); err != nil {
110		_ = os.Remove(archivePath)
111		_ = os.Remove(binaryPath)
112		return &ApplyUpdateOutput{
113			Status:  entity.UpdateStatusFailed,
114			Message: fmt.Sprintf("Staging failed: %v", err),
115		}, nil
116	}
117
118	// Clean up download artifacts.
119	_ = os.Remove(archivePath)
120	_ = os.Remove(binaryPath)
121
122	log.Info().Msg("update staged successfully, will apply on exit")
123
124	return &ApplyUpdateOutput{
125		Status:  entity.UpdateStatusReady,
126		Message: "Update ready - will apply on exit",
127	}, nil
128}
129
130// FinalizeOnExit applies the staged update during shutdown.
131// This should be called from the app's shutdown handler.
132func (uc *ApplyUpdateUseCase) FinalizeOnExit(ctx context.Context) error {
133	log := logging.FromContext(ctx)
134
135	if !uc.applier.HasStagedUpdate(ctx) {
136		log.Debug().Msg("no staged update to apply")
137		return nil
138	}
139
140	backupPath, err := uc.applier.ApplyOnExit(ctx)
141	if err != nil {
142		log.Error().Err(err).Msg("failed to apply update on exit")
143		return err
144	}
145
146	log.Info().
147		Str("backup", backupPath).
148		Msg("update applied successfully")
149
150	return nil
151}
152
153// HasPendingUpdate checks if there's an update waiting to be applied on exit.
154func (uc *ApplyUpdateUseCase) HasPendingUpdate(ctx context.Context) bool {
155	return uc.applier.HasStagedUpdate(ctx)
156}
157
158// ClearPendingUpdate removes any staged update.
159func (uc *ApplyUpdateUseCase) ClearPendingUpdate(ctx context.Context) error {
160	return uc.applier.ClearStagedUpdate(ctx)
161}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close