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