1package webkit
2
3import (
4 "context"
5 "fmt"
6 "sync"
7
8 "github.com/bnema/dumber/internal/application/port"
9 downloadutil "github.com/bnema/dumber/internal/domain/download"
10 "github.com/bnema/dumber/internal/infrastructure/downloadruntime"
11 "github.com/bnema/dumber/internal/logging"
12 "github.com/bnema/puregotk/v4/glib"
13 "github.com/bnema/puregotk/v4/webkit"
14)
15
16// DownloadHandler manages WebKit downloads and notifies the UI layer.
17type DownloadHandler struct {
18 runtime *downloadruntime.Runtime
19 mu sync.RWMutex
20}
21
22// NewDownloadHandler creates a new download handler.
23// The caller (ConfigureDownloads) must validate that preparer is non-nil.
24func NewDownloadHandler(
25 downloadPath string,
26 handler port.DownloadEventHandler,
27 preparer port.DownloadPreparer,
28) *DownloadHandler {
29 return &DownloadHandler{
30 runtime: downloadruntime.New(downloadPath, handler, preparer),
31 }
32}
33
34// downloadState tracks the state of a single download to prevent duplicate notifications.
35// This is allocated per-download to ensure proper isolation between concurrent downloads.
36type downloadState struct {
37 failed bool
38 filename string
39 destination string
40 lastProgressPercent int
41 mu sync.Mutex
42}
43
44// HandleDownload sets up signal handlers for a new download.
45func (h *DownloadHandler) HandleDownload(ctx context.Context, download *webkit.Download) {
46 if download == nil {
47 return
48 }
49
50 h.mu.RLock()
51 runtime := h.runtime
52 h.mu.RUnlock()
53
54 // Track download state in a dedicated struct to ensure proper isolation.
55 // Each download gets its own state to prevent interference between concurrent downloads.
56 state := &downloadState{lastProgressPercent: -1}
57
58 decideDestCb := makeDecideDestinationCallback(ctx, runtime, state)
59 download.ConnectDecideDestination(&decideDestCb)
60
61 progressCb := makeProgressCallback(ctx, runtime, state)
62 download.ConnectReceivedData(&progressCb)
63
64 failedCb := makeFailedCallback(ctx, runtime, state)
65 download.ConnectFailed(&failedCb)
66
67 finishedCb := makeFinishedCallback(ctx, runtime, state)
68 download.ConnectFinished(&finishedCb)
69}
70
71func makeDecideDestinationCallback(
72 ctx context.Context,
73 runtime *downloadruntime.Runtime,
74 state *downloadState,
75) func(webkit.Download, string) bool {
76 log := logging.FromContext(ctx)
77
78 return func(d webkit.Download, suggestedFilename string) bool {
79 var response port.DownloadResponse
80 if resp := d.GetResponse(); resp != nil {
81 response = &uriResponseAdapter{resp: resp}
82 }
83
84 output, err := runtime.ResolveDestination(ctx, suggestedFilename, response)
85 if err != nil {
86 log.Error().Err(err).Msg("failed to prepare download destination")
87 d.Cancel()
88 return false
89 }
90
91 log.Debug().
92 Str("suggested", suggestedFilename).
93 Str("sanitized", output.Filename).
94 Str("destPath", output.DestinationPath).
95 Msg("setting download destination")
96
97 d.SetDestination(output.DestinationPath)
98
99 state.mu.Lock()
100 state.filename = output.Filename
101 state.destination = output.DestinationPath
102 state.lastProgressPercent = -1
103 state.mu.Unlock()
104
105 runtime.EmitStarted(ctx, output)
106 emitWebKitDownloadProgress(ctx, runtime, state, d)
107 return false
108 }
109}
110
111func makeProgressCallback(
112 ctx context.Context,
113 runtime *downloadruntime.Runtime,
114 state *downloadState,
115) func(webkit.Download, uint64) {
116 return func(d webkit.Download, _ uint64) {
117 emitWebKitDownloadProgress(ctx, runtime, state, d)
118 }
119}
120
121func makeFailedCallback(
122 ctx context.Context,
123 runtime *downloadruntime.Runtime,
124 state *downloadState,
125) func(webkit.Download, *glib.Error) {
126 return func(d webkit.Download, gerr *glib.Error) {
127 state.mu.Lock()
128 state.failed = true
129 state.mu.Unlock()
130
131 dest := d.GetDestination()
132 filename := downloadutil.ExtractFilenameFromDestination(dest)
133 downloadErr := fmt.Errorf("download failed: %s: %s", filename, gerr.MessageGo())
134 runtime.EmitFailed(ctx, filename, dest, downloadErr)
135 }
136}
137
138func makeFinishedCallback(
139 ctx context.Context,
140 runtime *downloadruntime.Runtime,
141 state *downloadState,
142) func(webkit.Download) {
143 return func(d webkit.Download) {
144 state.mu.Lock()
145 failed := state.failed
146 state.mu.Unlock()
147 if failed {
148 return
149 }
150
151 dest := d.GetDestination()
152 filename := downloadutil.ExtractFilenameFromDestination(dest)
153 runtime.EmitFinished(ctx, filename, dest)
154 }
155}
156
157func emitWebKitDownloadProgress(
158 ctx context.Context,
159 runtime *downloadruntime.Runtime,
160 state *downloadState,
161 d webkit.Download,
162) {
163 state.mu.Lock()
164 if state.failed {
165 state.mu.Unlock()
166 return
167 }
168 filename := state.filename
169 destination := state.destination
170 lastPercent := state.lastProgressPercent
171 state.mu.Unlock()
172 if filename == "" || destination == "" {
173 return
174 }
175
176 progress := d.GetEstimatedProgress()
177 if progress <= 0 || progress >= 1 {
178 return
179 }
180 percent := int(progress * 100)
181 if percent <= lastPercent {
182 return
183 }
184
185 state.mu.Lock()
186 if state.failed || percent <= state.lastProgressPercent {
187 state.mu.Unlock()
188 return
189 }
190 state.lastProgressPercent = percent
191 state.mu.Unlock()
192
193 runtime.EmitProgress(ctx, filename, destination, progress, int64(d.GetReceivedDataLength()), 0)
194}
195
196// SetDownloadPath updates the download directory.
197func (h *DownloadHandler) SetDownloadPath(path string) {
198 h.mu.Lock()
199 defer h.mu.Unlock()
200 h.runtime.SetDownloadPath(path)
201}
202
203// uriResponseAdapter wraps webkit.URIResponse to implement port.DownloadResponse.
204type uriResponseAdapter struct {
205 resp *webkit.URIResponse
206}
207
208func (a *uriResponseAdapter) GetMimeType() string {
209 if a.resp == nil {
210 return ""
211 }
212 return a.resp.GetMimeType()
213}
214
215func (a *uriResponseAdapter) GetSuggestedFilename() string {
216 if a.resp == nil {
217 return ""
218 }
219 return a.resp.GetSuggestedFilename()
220}
221
222func (a *uriResponseAdapter) GetUri() string {
223 if a.resp == nil {
224 return ""
225 }
226 return a.resp.GetUri()
227}