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}