1package webkit
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "sync"
8
9 "github.com/bnema/dumber/internal/application/port"
10 "github.com/bnema/dumber/internal/application/usecase"
11 downloadutil "github.com/bnema/dumber/internal/domain/download"
12 "github.com/bnema/dumber/internal/logging"
13 "github.com/bnema/puregotk-webkit/webkit"
14)
15
16const (
17 // dirPerm is the permission mode for creating download directories.
18 dirPerm = 0755
19)
20
21// DownloadHandler manages WebKit downloads and notifies the UI layer.
22type DownloadHandler struct {
23 downloadPath string
24 eventHandler port.DownloadEventHandler
25 prepareDownloadUC *usecase.PrepareDownloadUseCase
26 mu sync.RWMutex
27}
28
29// NewDownloadHandler creates a new download handler.
30// Panics if prepareDownloadUC is nil (fail fast on misconfiguration).
31func NewDownloadHandler(
32 downloadPath string,
33 handler port.DownloadEventHandler,
34 prepareDownloadUC *usecase.PrepareDownloadUseCase,
35) *DownloadHandler {
36 if prepareDownloadUC == nil {
37 panic("prepareDownloadUC is required")
38 }
39 return &DownloadHandler{
40 downloadPath: downloadPath,
41 eventHandler: handler,
42 prepareDownloadUC: prepareDownloadUC,
43 }
44}
45
46// downloadState tracks the state of a single download to prevent duplicate notifications.
47// This is allocated per-download to ensure proper isolation between concurrent downloads.
48type downloadState struct {
49 failed bool
50 mu sync.Mutex
51}
52
53// HandleDownload sets up signal handlers for a new download.
54func (h *DownloadHandler) HandleDownload(ctx context.Context, download *webkit.Download) {
55 log := logging.FromContext(ctx)
56
57 h.mu.RLock()
58 downloadPath := h.downloadPath
59 eventHandler := h.eventHandler
60 prepareDownloadUC := h.prepareDownloadUC
61 h.mu.RUnlock()
62
63 // Track download state in a dedicated struct to ensure proper isolation.
64 // Each download gets its own state to prevent interference between concurrent downloads.
65 state := &downloadState{}
66
67 // Handle decide-destination signal to set download path.
68 //nolint:unparam // bool return is required by WebKit's signal signature (false = handled synchronously)
69 decideDestCb := func(d webkit.Download, suggestedFilename string) bool {
70 // Ensure download directory exists.
71 if err := os.MkdirAll(downloadPath, dirPerm); err != nil {
72 log.Error().Err(err).Str("path", downloadPath).Msg("failed to create download directory")
73 d.Cancel()
74 return false
75 }
76
77 // Wrap WebKit response as port.DownloadResponse
78 var response port.DownloadResponse
79 if resp := d.GetResponse(); resp != nil {
80 response = &uriResponseAdapter{resp: resp}
81 }
82
83 // Use PrepareDownloadUseCase to resolve filename
84 output := prepareDownloadUC.Execute(ctx, usecase.PrepareDownloadInput{
85 SuggestedFilename: suggestedFilename,
86 Response: response,
87 DownloadDir: downloadPath,
88 })
89
90 log.Debug().
91 Str("suggested", suggestedFilename).
92 Str("sanitized", output.Filename).
93 Str("destPath", output.DestinationPath).
94 Msg("setting download destination")
95
96 // WebKit expects an absolute file path for the destination.
97 d.SetDestination(output.DestinationPath)
98
99 // Notify: download started.
100 if eventHandler != nil {
101 eventHandler.OnDownloadEvent(ctx, port.DownloadEvent{
102 Type: port.DownloadEventStarted,
103 Filename: output.Filename,
104 Destination: output.DestinationPath,
105 })
106 }
107
108 log.Info().
109 Str("filename", output.Filename).
110 Str("destination", output.DestinationPath).
111 Msg("download started")
112
113 return false // false = we handled it synchronously
114 }
115 download.ConnectDecideDestination(&decideDestCb)
116
117 // Handle failed signal.
118 failedCb := func(d webkit.Download, _ uintptr) {
119 state.mu.Lock()
120 state.failed = true
121 state.mu.Unlock()
122
123 dest := d.GetDestination()
124 filename := downloadutil.ExtractFilenameFromDestination(dest)
125
126 // Create error for the failed download.
127 downloadErr := fmt.Errorf("download failed: %s", filename)
128
129 if eventHandler != nil {
130 eventHandler.OnDownloadEvent(ctx, port.DownloadEvent{
131 Type: port.DownloadEventFailed,
132 Filename: filename,
133 Destination: dest,
134 Error: downloadErr,
135 })
136 }
137
138 log.Warn().Str("filename", filename).Msg("download failed")
139 }
140 download.ConnectFailed(&failedCb)
141
142 // Handle finished signal.
143 finishedCb := func(d webkit.Download) {
144 state.mu.Lock()
145 failed := state.failed
146 state.mu.Unlock()
147
148 // Don't send finished event if we already sent a failed event.
149 if failed {
150 return
151 }
152
153 dest := d.GetDestination()
154 filename := downloadutil.ExtractFilenameFromDestination(dest)
155
156 if eventHandler != nil {
157 eventHandler.OnDownloadEvent(ctx, port.DownloadEvent{
158 Type: port.DownloadEventFinished,
159 Filename: filename,
160 Destination: dest,
161 })
162 }
163
164 log.Info().Str("filename", filename).Msg("download finished")
165 }
166 download.ConnectFinished(&finishedCb)
167}
168
169// SetDownloadPath updates the download directory.
170func (h *DownloadHandler) SetDownloadPath(path string) {
171 h.mu.Lock()
172 defer h.mu.Unlock()
173 h.downloadPath = path
174}
175
176// uriResponseAdapter wraps webkit.URIResponse to implement port.DownloadResponse.
177type uriResponseAdapter struct {
178 resp *webkit.URIResponse
179}
180
181func (a *uriResponseAdapter) GetMimeType() string {
182 if a.resp == nil {
183 return ""
184 }
185 return a.resp.GetMimeType()
186}
187
188func (a *uriResponseAdapter) GetSuggestedFilename() string {
189 if a.resp == nil {
190 return ""
191 }
192 return a.resp.GetSuggestedFilename()
193}
194
195func (a *uriResponseAdapter) GetUri() string {
196 if a.resp == nil {
197 return ""
198 }
199 return a.resp.GetUri()
200}