v0.21.0 73bf644

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		return &ApplyUpdateOutput{
62			Status:  entity.UpdateStatusFailed,
63			Message: "Cannot auto-update: binary is not writable",
64		}, nil
65	}
66
67	// Create download directory.
68	downloadDir := filepath.Join(uc.cacheDir, "updates")
69	if err := os.MkdirAll(downloadDir, updateDirPerm); err != nil {
70		return nil, fmt.Errorf("failed to create download directory: %w", err)
71	}
72
73	log.Info().Str("url", input.DownloadURL).Msg("downloading update")
74
75	// Download the archive.
76	archivePath, err := uc.downloader.Download(ctx, input.DownloadURL, downloadDir)
77	if err != nil {
78		return &ApplyUpdateOutput{
79			Status:  entity.UpdateStatusFailed,
80			Message: fmt.Sprintf("Download failed: %v", err),
81		}, nil
82	}
83
84	log.Info().Str("archive", archivePath).Msg("extracting update")
85
86	// Extract the binary.
87	binaryPath, err := uc.downloader.Extract(ctx, archivePath, downloadDir)
88	if err != nil {
89		_ = os.Remove(archivePath)
90		return &ApplyUpdateOutput{
91			Status:  entity.UpdateStatusFailed,
92			Message: fmt.Sprintf("Extraction failed: %v", err),
93		}, nil
94	}
95
96	log.Info().Str("binary", binaryPath).Msg("staging update")
97
98	// Stage the update.
99	if err := uc.applier.StageUpdate(ctx, binaryPath); err != nil {
100		_ = os.Remove(archivePath)
101		_ = os.Remove(binaryPath)
102		return &ApplyUpdateOutput{
103			Status:  entity.UpdateStatusFailed,
104			Message: fmt.Sprintf("Staging failed: %v", err),
105		}, nil
106	}
107
108	// Clean up download artifacts.
109	_ = os.Remove(archivePath)
110	_ = os.Remove(binaryPath)
111
112	log.Info().Msg("update staged successfully, will apply on exit")
113
114	return &ApplyUpdateOutput{
115		Status:  entity.UpdateStatusReady,
116		Message: "Update ready - will apply on exit",
117	}, nil
118}
119
120// FinalizeOnExit applies the staged update during shutdown.
121// This should be called from the app's shutdown handler.
122func (uc *ApplyUpdateUseCase) FinalizeOnExit(ctx context.Context) error {
123	log := logging.FromContext(ctx)
124
125	if !uc.applier.HasStagedUpdate(ctx) {
126		log.Debug().Msg("no staged update to apply")
127		return nil
128	}
129
130	backupPath, err := uc.applier.ApplyOnExit(ctx)
131	if err != nil {
132		log.Error().Err(err).Msg("failed to apply update on exit")
133		return err
134	}
135
136	log.Info().
137		Str("backup", backupPath).
138		Msg("update applied successfully")
139
140	return nil
141}
142
143// HasPendingUpdate checks if there's an update waiting to be applied on exit.
144func (uc *ApplyUpdateUseCase) HasPendingUpdate(ctx context.Context) bool {
145	return uc.applier.HasStagedUpdate(ctx)
146}
147
148// ClearPendingUpdate removes any staged update.
149func (uc *ApplyUpdateUseCase) ClearPendingUpdate(ctx context.Context) error {
150	return uc.applier.ClearStagedUpdate(ctx)
151}

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