repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

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
A pkg/shared/io.go
+35, -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+}
A pkg/shared/util.go
+188, -0
  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+}