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// preferredExtensions maps MIME types whose stdlib extension list is
50// platform-dependent (alphabetical order) to the canonical extension.
51// Without this, mime.ExtensionsByType("text/html") may return ".ehtml"
52// instead of ".html" depending on the system MIME database.
53var preferredExtensions = map[string]string{
54	"text/html":                ".html",
55	"text/plain":               ".txt",
56	"text/xml":                 ".xml",
57	"application/xhtml+xml":    ".xhtml",
58	"image/jpeg":               ".jpg",
59	"image/svg+xml":            ".svg",
60	"audio/mpeg":               ".mp3",
61	"video/mp4":                ".mp4",
62	"application/octet-stream": ".bin",
63}
64
65// GetExtensionFromMimeType returns a file extension for a given MIME type.
66// Returns empty string if MIME type is unknown or empty.
67// Handles MIME types with parameters (e.g., "application/pdf; charset=binary").
68func GetExtensionFromMimeType(mimeType string) string {
69	if mimeType == "" {
70		return ""
71	}
72
73	// Parse media type to extract bare type without parameters.
74	// mime.ExtensionsByType expects "application/pdf", not "application/pdf; charset=binary".
75	mediaType, _, err := mime.ParseMediaType(mimeType)
76	if err != nil || mediaType == "" {
77		return ""
78	}
79
80	if ext, ok := preferredExtensions[mediaType]; ok {
81		return ext
82	}
83
84	exts, err := mime.ExtensionsByType(mediaType)
85	if err != nil || len(exts) == 0 {
86		return ""
87	}
88
89	return exts[0]
90}
91
92// ExtractFilenameFromURI extracts the filename from a URI path component.
93// Handles both URIs and plain paths. Returns DefaultFilename for edge cases.
94func ExtractFilenameFromURI(uri string) string {
95	if uri == "" {
96		return DefaultFilename
97	}
98
99	parsed, err := url.Parse(uri)
100	if err != nil {
101		// Fall back to treating as plain path
102		return extractFromPath(uri)
103	}
104
105	return extractFromPath(parsed.Path)
106}
107
108// ExtractFilenameFromDestination extracts the filename from a file:// URI or path.
109// Used to get filename from WebKit download destination.
110func ExtractFilenameFromDestination(dest string) string {
111	// Remove file:// prefix if present.
112	path := strings.TrimPrefix(dest, "file://")
113	base := filepath.Base(path)
114
115	// Handle edge cases consistently with SanitizeFilename.
116	if base == "." || base == "" {
117		return DefaultFilename
118	}
119	return base
120}
121
122// extractFromPath extracts filename from a path string.
123func extractFromPath(path string) string {
124	base := filepath.Base(path)
125	if base == "." || base == "" || base == "/" {
126		return DefaultFilename
127	}
128	return base
129}
130
131// MakeUniqueFilename generates a unique filename by appending _(N) if needed.
132// The exists function should return true if the given path already exists.
133func MakeUniqueFilename(dir, filename string, exists func(path string) bool) string {
134	destPath := filepath.Join(dir, filename)
135	if !exists(destPath) {
136		return filename
137	}
138
139	// Split filename into base and extension
140	ext := filepath.Ext(filename)
141	base := strings.TrimSuffix(filename, ext)
142
143	// Try incrementing numbers until we find a unique name
144	for i := 1; i < 1000; i++ {
145		candidate := fmt.Sprintf("%s_(%d)%s", base, i, ext)
146		candidatePath := filepath.Join(dir, candidate)
147		if !exists(candidatePath) {
148			return candidate
149		}
150	}
151
152	// Fallback: use timestamp
153	candidate := fmt.Sprintf("%s_%d%s", base, time.Now().UnixNano(), ext)
154	return candidate
155}

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