filename.go RawHistoryFolder
1package download
2
3import (
4 "fmt"
5 "mime"
6 "net/url"
7 "path/filepath"
8 "strings"
9 "time"
10)
11
12const (
13 // DefaultFilename is used when no valid filename can be determined.
14 DefaultFilename = "download"
15)
16
17// SanitizeFilename sanitizes a filename to prevent path traversal attacks.
18// It extracts only the base name and handles edge cases like "." or "..".
19func SanitizeFilename(name string) string {
20 // Normalize Windows-style separators to forward slashes.
21 // filepath.Base only handles the OS-native separator, so on Linux
22 // backslashes would not be treated as path separators.
23 name = strings.ReplaceAll(name, "\\", "/")
24
25 // Get only the base name (removes any directory components).
26 clean := filepath.Base(name)
27
28 // If Base returns "." or ".." (edge cases), use fallback.
29 if clean == "." || clean == ".." || clean == "" {
30 return DefaultFilename
31 }
32
33 return clean
34}
35
36// SanitizeFilenameWithExtension sanitizes a filename and adds an extension
37// inferred from the MIME type if the filename has no extension.
38func SanitizeFilenameWithExtension(name, mimeType string) string {
39 clean := SanitizeFilename(name)
40 if filepath.Ext(clean) == "" {
41 if ext := GetExtensionFromMimeType(mimeType); ext != "" {
42 return clean + ext
43 }
44 }
45
46 return clean
47}
48
49// GetExtensionFromMimeType returns a file extension for a given MIME type.
50// Returns empty string if MIME type is unknown or empty.
51// Handles MIME types with parameters (e.g., "application/pdf; charset=binary").
52func GetExtensionFromMimeType(mimeType string) string {
53 if mimeType == "" {
54 return ""
55 }
56
57 // Parse media type to extract bare type without parameters.
58 // mime.ExtensionsByType expects "application/pdf", not "application/pdf; charset=binary".
59 mediaType, _, err := mime.ParseMediaType(mimeType)
60 if err != nil || mediaType == "" {
61 return ""
62 }
63
64 exts, err := mime.ExtensionsByType(mediaType)
65 if err != nil || len(exts) == 0 {
66 return ""
67 }
68
69 return exts[0]
70}
71
72// ExtractFilenameFromURI extracts the filename from a URI path component.
73// Handles both URIs and plain paths. Returns DefaultFilename for edge cases.
74func ExtractFilenameFromURI(uri string) string {
75 if uri == "" {
76 return DefaultFilename
77 }
78
79 parsed, err := url.Parse(uri)
80 if err != nil {
81 // Fall back to treating as plain path
82 return extractFromPath(uri)
83 }
84
85 return extractFromPath(parsed.Path)
86}
87
88// ExtractFilenameFromDestination extracts the filename from a file:// URI or path.
89// Used to get filename from WebKit download destination.
90func ExtractFilenameFromDestination(dest string) string {
91 // Remove file:// prefix if present.
92 path := strings.TrimPrefix(dest, "file://")
93 base := filepath.Base(path)
94
95 // Handle edge cases consistently with SanitizeFilename.
96 if base == "." || base == "" {
97 return DefaultFilename
98 }
99 return base
100}
101
102// extractFromPath extracts filename from a path string.
103func extractFromPath(path string) string {
104 base := filepath.Base(path)
105 if base == "." || base == "" || base == "/" {
106 return DefaultFilename
107 }
108 return base
109}
110
111// MakeUniqueFilename generates a unique filename by appending _(N) if needed.
112// The exists function should return true if the given path already exists.
113func MakeUniqueFilename(dir, filename string, exists func(path string) bool) string {
114 destPath := filepath.Join(dir, filename)
115 if !exists(destPath) {
116 return filename
117 }
118
119 // Split filename into base and extension
120 ext := filepath.Ext(filename)
121 base := strings.TrimSuffix(filename, ext)
122
123 // Try incrementing numbers until we find a unique name
124 for i := 1; i < 1000; i++ {
125 candidate := fmt.Sprintf("%s_(%d)%s", base, i, ext)
126 candidatePath := filepath.Join(dir, candidate)
127 if !exists(candidatePath) {
128 return candidate
129 }
130 }
131
132 // Fallback: use timestamp
133 candidate := fmt.Sprintf("%s_%d%s", base, time.Now().UnixNano(), ext)
134 return candidate
135}