v0.26.1 75cd777

download_handler.go RawHistoryFolder

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}

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