v0.26.2 3cd9d2e

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}

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