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}