apps/blobs/util.go 3.4 K raw
1
package main
2
3
import (
4
	"net/url"
5
	"path"
6
	"strings"
7
)
8
9
func isImageName(name string) bool {
10
	switch strings.ToLower(path.Ext(name)) {
11
	case ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".avif", ".bmp", ".ico":
12
		return true
13
	}
14
	return false
15
}
16
17
func humanSize(n int64) string {
18
	const k = 1024
19
	if n < k {
20
		return formatInt(n) + " B"
21
	}
22
	v := float64(n) / float64(k)
23
	if v < k {
24
		return formatFloat1(v) + " KB"
25
	}
26
	v /= k
27
	if v < k {
28
		return formatFloat1(v) + " MB"
29
	}
30
	v /= k
31
	return formatFloat1(v) + " GB"
32
}
33
34
func formatInt(n int64) string {
35
	if n == 0 {
36
		return "0"
37
	}
38
	neg := n < 0
39
	if neg {
40
		n = -n
41
	}
42
	buf := [24]byte{}
43
	i := len(buf)
44
	for n > 0 {
45
		i--
46
		buf[i] = byte('0' + n%10)
47
		n /= 10
48
	}
49
	if neg {
50
		i--
51
		buf[i] = '-'
52
	}
53
	return string(buf[i:])
54
}
55
56
func formatFloat1(v float64) string {
57
	scaled := int64(v*10 + 0.5)
58
	whole := scaled / 10
59
	frac := scaled % 10
60
	return formatInt(whole) + "." + string(byte('0'+frac))
61
}
62
63
// escapeKeyPath URL-escapes each path segment of an S3 key for use in app routes.
64
// Slashes are preserved as separators.
65
func escapeKeyPath(key string) string {
66
	parts := strings.Split(key, "/")
67
	for i, p := range parts {
68
		parts[i] = url.PathEscape(p)
69
	}
70
	return strings.Join(parts, "/")
71
}
72
73
// browseHref builds /b/{bucket}/browse/{prefix} (trailing slash preserved).
74
func browseHref(bucket, prefix string) string {
75
	if prefix == "" {
76
		return "/b/" + url.PathEscape(bucket) + "/browse/"
77
	}
78
	return "/b/" + url.PathEscape(bucket) + "/browse/" + escapeKeyPath(prefix)
79
}
80
81
func objectHref(bucket, key string) string {
82
	return "/b/" + url.PathEscape(bucket) + "/object/" + escapeKeyPath(key)
83
}
84
85
func previewHref(bucket, key string) string {
86
	return "/b/" + url.PathEscape(bucket) + "/preview/" + escapeKeyPath(key)
87
}
88
89
// buildCrumbs builds breadcrumb entries for the given bucket + prefix.
90
// The bucket crumb links to the bucket root; each segment links to its
91
// prefix browse URL.
92
func buildCrumbs(bucket, prefix string) []crumb {
93
	out := []crumb{{Label: bucket, Href: browseHref(bucket, "")}}
94
	if prefix == "" {
95
		return out
96
	}
97
	trimmed := strings.TrimSuffix(prefix, "/")
98
	parts := strings.Split(trimmed, "/")
99
	acc := ""
100
	for _, p := range parts {
101
		if p == "" {
102
			continue
103
		}
104
		acc += p + "/"
105
		out = append(out, crumb{Label: p, Href: browseHref(bucket, acc)})
106
	}
107
	return out
108
}
109
110
// parentPrefix returns the prefix one level up from the given prefix (which
111
// is expected to end in "/" or be empty). For object keys, use parentOfKey.
112
func parentPrefix(prefix string) string {
113
	trimmed := strings.TrimSuffix(prefix, "/")
114
	if trimmed == "" {
115
		return ""
116
	}
117
	i := strings.LastIndex(trimmed, "/")
118
	if i < 0 {
119
		return ""
120
	}
121
	return trimmed[:i+1]
122
}
123
124
func parentOfKey(key string) string {
125
	i := strings.LastIndex(key, "/")
126
	if i < 0 {
127
		return ""
128
	}
129
	return key[:i+1]
130
}
131
132
func nameOfKey(key string) string {
133
	i := strings.LastIndex(key, "/")
134
	if i < 0 {
135
		return key
136
	}
137
	return key[i+1:]
138
}
139
140
// parsePublicURLs parses a "bucket=url,bucket=url" string into a map.
141
func parsePublicURLs(raw string) map[string]string {
142
	out := map[string]string{}
143
	if strings.TrimSpace(raw) == "" {
144
		return out
145
	}
146
	for _, pair := range strings.Split(raw, ",") {
147
		pair = strings.TrimSpace(pair)
148
		if pair == "" {
149
			continue
150
		}
151
		eq := strings.IndexByte(pair, '=')
152
		if eq <= 0 {
153
			continue
154
		}
155
		k := strings.TrimSpace(pair[:eq])
156
		v := strings.TrimSpace(pair[eq+1:])
157
		if k != "" && v != "" {
158
			out[k] = v
159
		}
160
	}
161
	return out
162
}