prepare_download.go RawHistoryFolder
1package usecase
2
3import (
4 "context"
5 "path/filepath"
6
7 "github.com/bnema/dumber/internal/application/port"
8 "github.com/bnema/dumber/internal/domain/download"
9 "github.com/bnema/dumber/internal/logging"
10)
11
12// PrepareDownloadInput is an alias for port.DownloadPrepareInput.
13type PrepareDownloadInput = port.DownloadPrepareInput
14
15// PrepareDownloadOutput is an alias for port.DownloadPrepareOutput.
16type PrepareDownloadOutput = port.DownloadPrepareOutput
17
18// Compile-time check: PrepareDownloadUseCase must satisfy port.DownloadPreparer.
19var _ port.DownloadPreparer = (*PrepareDownloadUseCase)(nil)
20
21// PrepareDownloadUseCase orchestrates filename resolution for downloads.
22// It combines multiple sources (suggested name, response headers, URI) to
23// determine the best filename, then sanitizes it to prevent path traversal.
24type PrepareDownloadUseCase struct {
25 fs port.FileSystem
26}
27
28// NewPrepareDownloadUseCase creates a new PrepareDownloadUseCase.
29// If fs is nil, filename deduplication is disabled.
30func NewPrepareDownloadUseCase(fs port.FileSystem) *PrepareDownloadUseCase {
31 return &PrepareDownloadUseCase{fs: fs}
32}
33
34// Execute resolves the download filename and destination path.
35func (u *PrepareDownloadUseCase) Execute(ctx context.Context, input PrepareDownloadInput) *PrepareDownloadOutput {
36 log := logging.FromContext(ctx)
37
38 // Resolve filename from multiple sources (priority: suggested > response suggested > URI)
39 resolvedName := u.resolveSuggestedFilename(input.SuggestedFilename, input.Response)
40
41 // Get MIME type for extension inference
42 mimeType := ""
43 if input.Response != nil {
44 mimeType = input.Response.GetMimeType()
45 }
46
47 // Sanitize filename and add extension if needed
48 safeName := download.SanitizeFilenameWithExtension(resolvedName, mimeType)
49
50 // Make filename unique if file already exists
51 if u.fs != nil {
52 safeName = download.MakeUniqueFilename(input.DownloadDir, safeName, func(path string) bool {
53 exists, err := u.fs.Exists(ctx, path)
54 if err != nil {
55 log.Warn().Err(err).Str("path", path).
56 Msg("failed to check file existence; assuming exists")
57 return true // Conservative: assume exists to avoid overwrite
58 }
59 return exists
60 })
61 }
62
63 destPath := filepath.Join(input.DownloadDir, safeName)
64
65 log.Debug().
66 Str("suggested", input.SuggestedFilename).
67 Str("resolved", resolvedName).
68 Str("sanitized", safeName).
69 Str("destPath", destPath).
70 Msg("prepared download destination")
71
72 return &PrepareDownloadOutput{
73 Filename: safeName,
74 DestinationPath: destPath,
75 }
76}
77
78// resolveSuggestedFilename determines the best filename from available sources.
79func (*PrepareDownloadUseCase) resolveSuggestedFilename(name string, response port.DownloadResponse) string {
80 // Priority 1: Use provided suggested filename
81 if name != "" {
82 return name
83 }
84
85 if response == nil {
86 return name
87 }
88
89 // Priority 2: Use response's suggested filename (from Content-Disposition)
90 if suggested := response.GetSuggestedFilename(); suggested != "" {
91 return suggested
92 }
93
94 // Priority 3: Extract from URI path
95 uri := response.GetUri()
96 if uri == "" {
97 return name
98 }
99
100 return download.ExtractFilenameFromURI(uri)
101}