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 contains the inputs for preparing a download destination.
13type PrepareDownloadInput struct {
14 // SuggestedFilename is the filename suggested by WebKit (from Content-Disposition or other sources).
15 SuggestedFilename string
16 // Response provides additional metadata (MIME type, URI) for filename resolution.
17 // May be nil if not available.
18 Response port.DownloadResponse
19 // DownloadDir is the directory where downloads should be saved.
20 DownloadDir string
21}
22
23// PrepareDownloadOutput contains the resolved download destination.
24type PrepareDownloadOutput struct {
25 // Filename is the sanitized, safe filename to use.
26 Filename string
27 // DestinationPath is the full path where the download should be saved.
28 DestinationPath string
29}
30
31// PrepareDownloadUseCase orchestrates filename resolution for downloads.
32// It combines multiple sources (suggested name, response headers, URI) to
33// determine the best filename, then sanitizes it to prevent path traversal.
34type PrepareDownloadUseCase struct {
35 fs port.FileSystem
36}
37
38// NewPrepareDownloadUseCase creates a new PrepareDownloadUseCase.
39// If fs is nil, filename deduplication is disabled.
40func NewPrepareDownloadUseCase(fs port.FileSystem) *PrepareDownloadUseCase {
41 return &PrepareDownloadUseCase{fs: fs}
42}
43
44// Execute resolves the download filename and destination path.
45func (u *PrepareDownloadUseCase) Execute(ctx context.Context, input PrepareDownloadInput) *PrepareDownloadOutput {
46 log := logging.FromContext(ctx)
47
48 // Resolve filename from multiple sources (priority: suggested > response suggested > URI)
49 resolvedName := u.resolveSuggestedFilename(input.SuggestedFilename, input.Response)
50
51 // Get MIME type for extension inference
52 mimeType := ""
53 if input.Response != nil {
54 mimeType = input.Response.GetMimeType()
55 }
56
57 // Sanitize filename and add extension if needed
58 safeName := download.SanitizeFilenameWithExtension(resolvedName, mimeType)
59
60 // Make filename unique if file already exists
61 if u.fs != nil {
62 safeName = download.MakeUniqueFilename(input.DownloadDir, safeName, func(path string) bool {
63 exists, err := u.fs.Exists(ctx, path)
64 if err != nil {
65 log.Warn().Err(err).Str("path", path).
66 Msg("failed to check file existence; assuming exists")
67 return true // Conservative: assume exists to avoid overwrite
68 }
69 return exists
70 })
71 }
72
73 destPath := filepath.Join(input.DownloadDir, safeName)
74
75 log.Debug().
76 Str("suggested", input.SuggestedFilename).
77 Str("resolved", resolvedName).
78 Str("sanitized", safeName).
79 Str("destPath", destPath).
80 Msg("prepared download destination")
81
82 return &PrepareDownloadOutput{
83 Filename: safeName,
84 DestinationPath: destPath,
85 }
86}
87
88// resolveSuggestedFilename determines the best filename from available sources.
89func (*PrepareDownloadUseCase) resolveSuggestedFilename(name string, response port.DownloadResponse) string {
90 // Priority 1: Use provided suggested filename
91 if name != "" {
92 return name
93 }
94
95 if response == nil {
96 return name
97 }
98
99 // Priority 2: Use response's suggested filename (from Content-Disposition)
100 if suggested := response.GetSuggestedFilename(); suggested != "" {
101 return suggested
102 }
103
104 // Priority 3: Extract from URI path
105 uri := response.GetUri()
106 if uri == "" {
107 return name
108 }
109
110 return download.ExtractFilenameFromURI(uri)
111}