Commit bccde8d

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