v0.31.0 bf0756b

runtime.go RawHistoryFolder

1package downloadruntime
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"os"
8	"sync"
9
10	"github.com/bnema/dumber/internal/application/port"
11	"github.com/bnema/dumber/internal/logging"
12)
13
14const dirPerm = 0o755
15
16type codedDownloadError interface {
17	DownloadErrorCode() int
18	DownloadErrorReason() string
19}
20
21// Runtime shares common download preparation and event emission logic across engines.
22type Runtime struct {
23	mu           sync.RWMutex
24	downloadPath string
25	eventHandler port.DownloadEventHandler
26	preparer     port.DownloadPreparer
27}
28
29// New creates a Runtime. The caller must ensure preparer is non-nil;
30// ConfigureDownloads validates this before reaching here.
31func New(downloadPath string, eventHandler port.DownloadEventHandler, preparer port.DownloadPreparer) *Runtime {
32	return &Runtime{
33		downloadPath: downloadPath,
34		eventHandler: eventHandler,
35		preparer:     preparer,
36	}
37}
38
39func (r *Runtime) SetDownloadPath(path string) {
40	r.mu.Lock()
41	defer r.mu.Unlock()
42	r.downloadPath = path
43}
44
45func (r *Runtime) ResolveDestination(
46	ctx context.Context,
47	suggestedFilename string,
48	response port.DownloadResponse,
49) (*port.DownloadPrepareOutput, error) {
50	r.mu.RLock()
51	downloadPath := r.downloadPath
52	preparer := r.preparer
53	r.mu.RUnlock()
54
55	if preparer == nil {
56		return nil, fmt.Errorf("download preparer is required")
57	}
58
59	if err := os.MkdirAll(downloadPath, dirPerm); err != nil {
60		return nil, fmt.Errorf("failed to create download directory %q: %w", downloadPath, err)
61	}
62
63	output := preparer.Execute(ctx, port.DownloadPrepareInput{
64		SuggestedFilename: suggestedFilename,
65		Response:          response,
66		DownloadDir:       downloadPath,
67	})
68	if output == nil {
69		return nil, fmt.Errorf("failed to prepare download destination for %q", suggestedFilename)
70	}
71
72	return output, nil
73}
74
75func (r *Runtime) EmitStarted(ctx context.Context, output *port.DownloadPrepareOutput) {
76	if output == nil {
77		return
78	}
79
80	r.mu.RLock()
81	eventHandler := r.eventHandler
82	r.mu.RUnlock()
83
84	if eventHandler != nil {
85		eventHandler.OnDownloadEvent(ctx, port.DownloadEvent{
86			Type:        port.DownloadEventStarted,
87			Filename:    output.Filename,
88			Destination: output.DestinationPath,
89		})
90	}
91
92	logging.FromContext(ctx).Info().
93		Str("filename", output.Filename).
94		Str("destination", output.DestinationPath).
95		Msg("download started")
96}
97
98func (r *Runtime) EmitProgress(
99	ctx context.Context,
100	filename, destination string,
101	progress float64,
102	bytesReceived, bytesTotal int64,
103) {
104	r.mu.RLock()
105	eventHandler := r.eventHandler
106	r.mu.RUnlock()
107
108	if eventHandler != nil {
109		eventHandler.OnDownloadEvent(ctx, port.DownloadEvent{
110			Type:          port.DownloadEventProgress,
111			Filename:      filename,
112			Destination:   destination,
113			Progress:      progress,
114			BytesReceived: bytesReceived,
115			BytesTotal:    bytesTotal,
116		})
117	}
118}
119
120func (r *Runtime) EmitFinished(ctx context.Context, filename, destination string) {
121	r.mu.RLock()
122	eventHandler := r.eventHandler
123	r.mu.RUnlock()
124
125	if eventHandler != nil {
126		eventHandler.OnDownloadEvent(ctx, port.DownloadEvent{
127			Type:        port.DownloadEventFinished,
128			Filename:    filename,
129			Destination: destination,
130		})
131	}
132
133	logging.FromContext(ctx).Info().
134		Str("filename", filename).
135		Str("destination", destination).
136		Msg("download finished")
137}
138
139func (r *Runtime) EmitFailed(ctx context.Context, filename, destination string, err error) {
140	r.mu.RLock()
141	eventHandler := r.eventHandler
142	r.mu.RUnlock()
143
144	if eventHandler != nil {
145		eventHandler.OnDownloadEvent(ctx, port.DownloadEvent{
146			Type:        port.DownloadEventFailed,
147			Filename:    filename,
148			Destination: destination,
149			Error:       err,
150		})
151	}
152
153	log := logging.FromContext(ctx).Warn().
154		Err(err).
155		Str("filename", filename).
156		Str("destination", destination)
157	var codedErr codedDownloadError
158	if errors.As(err, &codedErr) {
159		log = log.
160			Int("error_code", codedErr.DownloadErrorCode()).
161			Str("error_reason", codedErr.DownloadErrorReason())
162	}
163	log.Msg("download failed")
164}

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