Antonio Mika
·
2026-10-10
1package shared
2
3import (
4 "crypto/sha256"
5 "encoding/base64"
6 "encoding/hex"
7 "fmt"
8 "math"
9 "os"
10 "path"
11 "path/filepath"
12 "regexp"
13 "strings"
14 "time"
15 "unicode"
16 "unicode/utf8"
17
18 "slices"
19
20 "golang.org/x/crypto/ssh"
21)
22
23var fnameRe = regexp.MustCompile(`[-_]+`)
24var subdomainRe = regexp.MustCompile(`^[a-z0-9-]+$`)
25
26var KB = 1000
27var MB = KB * 1000
28
29func IsValidSubdomain(subd string) bool {
30 return subdomainRe.MatchString(subd)
31}
32
33func FilenameToTitle(filename string, title string) string {
34 if filename != title {
35 return title
36 }
37
38 return ToUpper(title)
39}
40
41func ToUpper(str string) string {
42 pre := fnameRe.ReplaceAllString(str, " ")
43
44 r := []rune(pre)
45 if len(r) > 0 {
46 r[0] = unicode.ToUpper(r[0])
47 }
48
49 return string(r)
50}
51
52func SanitizeFileExt(fname string) string {
53 return strings.TrimSuffix(fname, filepath.Ext(fname))
54}
55
56func KeyForKeyText(pk ssh.PublicKey) string {
57 kb := base64.StdEncoding.EncodeToString(pk.Marshal())
58 return fmt.Sprintf("%s %s", pk.Type(), kb)
59}
60
61func GetEnv(key string, defaultVal string) string {
62 if value, exists := os.LookupEnv(key); exists {
63 return value
64 }
65
66 return defaultVal
67}
68
69// IsText reports whether a significant prefix of s looks like correct UTF-8;
70// that is, if it is likely that s is human-readable text.
71func IsText(s string) bool {
72 const max = 1024 // at least utf8.UTFMax
73 if len(s) > max {
74 s = s[0:max]
75 }
76 for i, c := range s {
77 if i+utf8.UTFMax > len(s) {
78 // last char may be incomplete - ignore
79 break
80 }
81 if c == 0xFFFD || c < ' ' && c != '\n' && c != '\t' && c != '\f' && c != '\r' {
82 // decoding error or control character - not a text file
83 return false
84 }
85 }
86 return true
87}
88
89func IsExtAllowed(filename string, allowedExt []string) bool {
90 ext := path.Ext(filename)
91 return slices.Contains(allowedExt, ext)
92}
93
94// IsTextFile reports whether the file has a known extension indicating
95// a text file, or if a significant chunk of the specified file looks like
96// correct UTF-8; that is, if it is likely that the file contains human-
97// readable text.
98func IsTextFile(text string) bool {
99 num := math.Min(float64(len(text)), 1024)
100 return IsText(text[0:int(num)])
101}
102
103const solarYearSecs = 31556926
104
105func TimeAgo(t *time.Time) string {
106 d := time.Since(*t)
107 var metric string
108 var amount int
109 if d.Seconds() < 60 {
110 amount = int(d.Seconds())
111 metric = "second"
112 } else if d.Minutes() < 60 {
113 amount = int(d.Minutes())
114 metric = "minute"
115 } else if d.Hours() < 24 {
116 amount = int(d.Hours())
117 metric = "hour"
118 } else if d.Seconds() < solarYearSecs {
119 amount = int(d.Hours()) / 24
120 metric = "day"
121 } else {
122 amount = int(d.Seconds()) / solarYearSecs
123 metric = "year"
124 }
125 if amount == 1 {
126 return fmt.Sprintf("%d %s ago", amount, metric)
127 } else {
128 return fmt.Sprintf("%d %ss ago", amount, metric)
129 }
130}
131
132func Shasum(data []byte) string {
133 h := sha256.New()
134 h.Write(data)
135 bs := h.Sum(nil)
136 return hex.EncodeToString(bs)
137}
138
139func BytesToMB(size int) float32 {
140 return ((float32(size) / 1000) / 1000)
141}
142
143func BytesToGB(size int) float32 {
144 return BytesToMB(size) / 1000
145}
146
147// StorageUsage is the note an upload prints about storage. It is empty
148// until a user reaches their quota.
149func StorageUsage(used, max int) string {
150 if max <= 0 || used < max {
151 return ""
152 }
153 pct := float32(used) / float32(max) * 100
154 return fmt.Sprintf(" (space: %.2f/%.2fGB, %.2f%%)", BytesToGB(used), BytesToGB(max), pct)
155}
156
157// https://stackoverflow.com/a/46964105
158func StartOfMonth() time.Time {
159 now := time.Now()
160 firstday := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
161 return firstday
162}
163
164func StartOfYear() time.Time {
165 now := time.Now()
166 return now.AddDate(-1, 0, 0)
167}
168
169func AnyToStr(mp map[string]any, key string) string {
170 if value, ok := mp[key]; ok {
171 if value, ok := value.(string); ok {
172 return value
173 }
174 }
175 return ""
176}
177
178func AnyToFloat(mp map[string]any, key string) float64 {
179 if value, ok := mp[key]; ok {
180 if value, ok := value.(float64); ok {
181 return value
182 }
183 }
184 return 0
185}
186
187func AnyToBool(mp map[string]any, key string) bool {
188 if value, ok := mp[key]; ok {
189 if value, ok := value.(bool); ok {
190 return value
191 }
192 }
193 return false
194}