repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

commit
5b1432d
parent
dc5a309
author
Antonio Mika
date
2024-10-08 01:47:09 +0000 UTC
Refactor
56 files changed,  +456, -1040
M go.mod
M go.sum
M auth/api.go
+6, -5
 1@@ -18,6 +18,7 @@ import (
 2 	"github.com/picosh/pico/db"
 3 	"github.com/picosh/pico/db/postgres"
 4 	"github.com/picosh/pico/shared"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 type Client struct {
 9@@ -647,13 +648,13 @@ type AuthCfg struct {
10 }
11 
12 func StartApiServer() {
13-	debug := shared.GetEnv("AUTH_DEBUG", "0")
14+	debug := utils.GetEnv("AUTH_DEBUG", "0")
15 	cfg := &AuthCfg{
16-		DbURL:  shared.GetEnv("DATABASE_URL", ""),
17+		DbURL:  utils.GetEnv("DATABASE_URL", ""),
18 		Debug:  debug == "1",
19-		Issuer: shared.GetEnv("AUTH_ISSUER", "pico.sh"),
20-		Domain: shared.GetEnv("AUTH_DOMAIN", "http://0.0.0.0:3000"),
21-		Port:   shared.GetEnv("AUTH_WEB_PORT", "3000"),
22+		Issuer: utils.GetEnv("AUTH_ISSUER", "pico.sh"),
23+		Domain: utils.GetEnv("AUTH_DOMAIN", "http://0.0.0.0:3000"),
24+		Port:   utils.GetEnv("AUTH_WEB_PORT", "3000"),
25 	}
26 
27 	logger := shared.CreateLogger("auth")
M cmd/scripts/analytics/analytics.go
+2, -2
 1@@ -6,7 +6,7 @@ import (
 2 
 3 	"github.com/picosh/pico/db"
 4 	"github.com/picosh/pico/db/postgres"
 5-	"github.com/picosh/pico/shared"
 6+	"github.com/picosh/utils"
 7 )
 8 
 9 func main() {
10@@ -23,7 +23,7 @@ func main() {
11 			// By:   "post_id",
12 			By:       "user_id",
13 			Interval: "day",
14-			Origin:   shared.StartOfMonth(),
15+			Origin:   utils.StartOfMonth(),
16 			// Where:    "AND (post_id IS NOT NULL OR (post_id IS NULL AND project_id IS NULL))",
17 		},
18 	)
M cmd/scripts/clean-object-store/clean.go
+3, -2
 1@@ -10,6 +10,7 @@ import (
 2 	"github.com/picosh/pico/pgs"
 3 	"github.com/picosh/pico/shared"
 4 	"github.com/picosh/pico/shared/storage"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 func bail(err error) {
 9@@ -27,7 +28,7 @@ type RmProject struct {
10 // have a corresponding project inside our database.
11 func main() {
12 	// to actually commit changes, set to true
13-	writeEnv := shared.GetEnv("WRITE", "0")
14+	writeEnv := utils.GetEnv("WRITE", "0")
15 	write := false
16 	if writeEnv == "1" {
17 		write = true
18@@ -102,7 +103,7 @@ func main() {
19 		}
20 	}
21 
22-	session := &shared.CmdSessionLogger{
23+	session := &utils.CmdSessionLogger{
24 		Log: logger,
25 	}
26 
M cmd/scripts/shasum/shasum.go
+2, -1
 1@@ -6,6 +6,7 @@ import (
 2 
 3 	"github.com/picosh/pico/db/postgres"
 4 	"github.com/picosh/pico/shared"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 func main() {
 9@@ -24,7 +25,7 @@ func main() {
10 	empty := 0
11 	diff := 0
12 	for _, post := range posts {
13-		nextShasum := shared.Shasum([]byte(post.Text))
14+		nextShasum := utils.Shasum([]byte(post.Text))
15 		if post.Shasum == "" {
16 			empty += 1
17 		} else if post.Shasum != nextShasum {
M db/postgres/storage.go
+2, -2
 1@@ -14,7 +14,7 @@ import (
 2 
 3 	_ "github.com/lib/pq"
 4 	"github.com/picosh/pico/db"
 5-	"github.com/picosh/pico/shared"
 6+	"github.com/picosh/utils"
 7 )
 8 
 9 var PAGER_SIZE = 15
10@@ -1552,7 +1552,7 @@ func (me *PsqlDB) FindFeedItemsByPostID(postID string) ([]*db.FeedItem, error) {
11 }
12 
13 func (me *PsqlDB) InsertProject(userID, name, projectDir string) (string, error) {
14-	if !shared.IsValidSubdomain(name) {
15+	if !utils.IsValidSubdomain(name) {
16 		return "", fmt.Errorf("'%s' is not a valid project name, must match /^[a-z0-9-]+$/", name)
17 	}
18 
M docker-compose.prod.yml
+1, -1
1@@ -96,7 +96,7 @@ services:
2     env_file:
3       - .env.prod
4     environment:
5-      APP_DOMAIN: ${PUBSUB_DOMAIN:-send.pico.sh}
6+      APP_DOMAIN: ${PUBSUB_DOMAIN:-pipe.pico.sh}
7       APP_EMAIL: ${PUBSUB_EMAIL:-hello@pico.sh}
8     volumes:
9       - ${PUBSUB_CADDYFILE}:/etc/caddy/Caddyfile
M feeds/config.go
+11, -10
 1@@ -2,19 +2,20 @@ package feeds
 2 
 3 import (
 4 	"github.com/picosh/pico/shared"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 func NewConfigSite() *shared.ConfigSite {
 9-	debug := shared.GetEnv("FEEDS_DEBUG", "0")
10-	domain := shared.GetEnv("FEEDS_DOMAIN", "feeds.pico.sh")
11-	port := shared.GetEnv("FEEDS_WEB_PORT", "3000")
12-	protocol := shared.GetEnv("FEEDS_PROTOCOL", "https")
13-	storageDir := shared.GetEnv("IMGS_STORAGE_DIR", ".storage")
14-	minioURL := shared.GetEnv("MINIO_URL", "")
15-	minioUser := shared.GetEnv("MINIO_ROOT_USER", "")
16-	minioPass := shared.GetEnv("MINIO_ROOT_PASSWORD", "")
17-	dbURL := shared.GetEnv("DATABASE_URL", "")
18-	sendgridKey := shared.GetEnv("SENDGRID_API_KEY", "")
19+	debug := utils.GetEnv("FEEDS_DEBUG", "0")
20+	domain := utils.GetEnv("FEEDS_DOMAIN", "feeds.pico.sh")
21+	port := utils.GetEnv("FEEDS_WEB_PORT", "3000")
22+	protocol := utils.GetEnv("FEEDS_PROTOCOL", "https")
23+	storageDir := utils.GetEnv("IMGS_STORAGE_DIR", ".storage")
24+	minioURL := utils.GetEnv("MINIO_URL", "")
25+	minioUser := utils.GetEnv("MINIO_ROOT_USER", "")
26+	minioPass := utils.GetEnv("MINIO_ROOT_PASSWORD", "")
27+	dbURL := utils.GetEnv("DATABASE_URL", "")
28+	sendgridKey := utils.GetEnv("SENDGRID_API_KEY", "")
29 
30 	return &shared.ConfigSite{
31 		Debug:       debug == "1",
M feeds/scp_hooks.go
+4, -3
 1@@ -11,6 +11,7 @@ import (
 2 	"github.com/picosh/pico/db"
 3 	"github.com/picosh/pico/filehandlers"
 4 	"github.com/picosh/pico/shared"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 type FeedHooks struct {
 9@@ -19,7 +20,7 @@ type FeedHooks struct {
10 }
11 
12 func (p *FeedHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaData) (bool, error) {
13-	if !shared.IsTextFile(string(data.Text)) {
14+	if !utils.IsTextFile(string(data.Text)) {
15 		err := fmt.Errorf(
16 			"WARNING: (%s) invalid file must be plain text (utf-8), skipping",
17 			data.Filename,
18@@ -27,7 +28,7 @@ func (p *FeedHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaData)
19 		return false, err
20 	}
21 
22-	if !shared.IsExtAllowed(data.Filename, p.Cfg.AllowedExt) {
23+	if !utils.IsExtAllowed(data.Filename, p.Cfg.AllowedExt) {
24 		extStr := strings.Join(p.Cfg.AllowedExt, ",")
25 		err := fmt.Errorf(
26 			"WARNING: (%s) invalid file, format must be (%s), skipping",
27@@ -44,7 +45,7 @@ func (p *FeedHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData) err
28 	parsedText := shared.ListParseText(string(data.Text))
29 
30 	if parsedText.Title == "" {
31-		data.Title = shared.ToUpper(data.Slug)
32+		data.Title = utils.ToUpper(data.Slug)
33 	} else {
34 		data.Title = parsedText.Title
35 	}
M feeds/ssh.go
+8, -8
 1@@ -14,16 +14,16 @@ import (
 2 	"github.com/picosh/pico/db/postgres"
 3 	"github.com/picosh/pico/filehandlers"
 4 	"github.com/picosh/pico/filehandlers/util"
 5-	"github.com/picosh/pico/shared"
 6 	"github.com/picosh/pico/shared/storage"
 7 	wsh "github.com/picosh/pico/wish"
 8+	"github.com/picosh/send/auth"
 9 	"github.com/picosh/send/list"
10 	"github.com/picosh/send/pipe"
11+	wishrsync "github.com/picosh/send/protocols/rsync"
12+	"github.com/picosh/send/protocols/scp"
13+	"github.com/picosh/send/protocols/sftp"
14 	"github.com/picosh/send/proxy"
15-	"github.com/picosh/send/send/auth"
16-	wishrsync "github.com/picosh/send/send/rsync"
17-	"github.com/picosh/send/send/scp"
18-	"github.com/picosh/send/send/sftp"
19+	"github.com/picosh/utils"
20 )
21 
22 func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
23@@ -52,9 +52,9 @@ func withProxy(handler *filehandlers.FileHandlerRouter, otherMiddleware ...wish.
24 }
25 
26 func StartSshServer() {
27-	host := shared.GetEnv("LISTS_HOST", "0.0.0.0")
28-	port := shared.GetEnv("LISTS_SSH_PORT", "2222")
29-	promPort := shared.GetEnv("LISTS_PROM_PORT", "9222")
30+	host := utils.GetEnv("LISTS_HOST", "0.0.0.0")
31+	port := utils.GetEnv("LISTS_SSH_PORT", "2222")
32+	promPort := utils.GetEnv("LISTS_PROM_PORT", "9222")
33 	cfg := NewConfigSite()
34 	logger := cfg.Logger
35 	dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
M filehandlers/assets/handler.go
+14, -13
 1@@ -22,7 +22,8 @@ import (
 2 	"github.com/picosh/pico/shared/storage"
 3 	"github.com/picosh/pobj"
 4 	sst "github.com/picosh/pobj/storage"
 5-	"github.com/picosh/send/send/utils"
 6+	sendutils "github.com/picosh/send/utils"
 7+	"github.com/picosh/utils"
 8 	ignore "github.com/sabhiram/go-gitignore"
 9 )
10 
11@@ -91,7 +92,7 @@ func shouldIgnoreFile(fp, ignoreStr string) bool {
12 }
13 
14 type FileData struct {
15-	*utils.FileEntry
16+	*sendutils.FileEntry
17 	User     *db.User
18 	Bucket   sst.Bucket
19 	Project  *db.Project
20@@ -116,13 +117,13 @@ func (h *UploadAssetHandler) GetLogger() *slog.Logger {
21 	return h.Cfg.Logger
22 }
23 
24-func (h *UploadAssetHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileInfo, utils.ReaderAtCloser, error) {
25+func (h *UploadAssetHandler) Read(s ssh.Session, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReaderAtCloser, error) {
26 	user, err := futil.GetUser(s.Context())
27 	if err != nil {
28 		return nil, nil, err
29 	}
30 
31-	fileInfo := &utils.VirtualFile{
32+	fileInfo := &sendutils.VirtualFile{
33 		FName:    filepath.Base(entry.Filepath),
34 		FIsDir:   false,
35 		FSize:    entry.Size,
36@@ -170,7 +171,7 @@ func (h *UploadAssetHandler) List(s ssh.Session, fpath string, isDir bool, recur
37 			name = "/"
38 		}
39 
40-		info := &utils.VirtualFile{
41+		info := &sendutils.VirtualFile{
42 			FName:  name,
43 			FIsDir: true,
44 		}
45@@ -243,7 +244,7 @@ func (h *UploadAssetHandler) findDenylist(bucket sst.Bucket, project *db.Project
46 	return str, nil
47 }
48 
49-func (h *UploadAssetHandler) Write(s ssh.Session, entry *utils.FileEntry) (string, error) {
50+func (h *UploadAssetHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (string, error) {
51 	user, err := futil.GetUser(s.Context())
52 	if err != nil {
53 		h.Cfg.Logger.Error("user not found in ctx", "err", err.Error())
54@@ -365,7 +366,7 @@ func (h *UploadAssetHandler) Write(s ssh.Session, entry *utils.FileEntry) (strin
55 	)
56 
57 	fsize, err := h.writeAsset(
58-		shared.NewMaxBytesReader(data.Reader, int64(sizeRemaining)),
59+		utils.NewMaxBytesReader(data.Reader, int64(sizeRemaining)),
60 		data,
61 	)
62 	if err != nil {
63@@ -373,9 +374,9 @@ func (h *UploadAssetHandler) Write(s ssh.Session, entry *utils.FileEntry) (strin
64 		cerr := fmt.Errorf(
65 			"%s: storage size %.2fmb, storage max %.2fmb, file max %.2fmb",
66 			err,
67-			shared.BytesToMB(int(curStorageSize)),
68-			shared.BytesToMB(int(storageMax)),
69-			shared.BytesToMB(int(fileMax)),
70+			utils.BytesToMB(int(curStorageSize)),
71+			utils.BytesToMB(int(storageMax)),
72+			utils.BytesToMB(int(fileMax)),
73 		)
74 		return "", cerr
75 	}
76@@ -393,15 +394,15 @@ func (h *UploadAssetHandler) Write(s ssh.Session, entry *utils.FileEntry) (strin
77 	str := fmt.Sprintf(
78 		"%s (space: %.2f/%.2fGB, %.2f%%)",
79 		url,
80-		shared.BytesToGB(int(nextStorageSize)),
81-		shared.BytesToGB(maxSize),
82+		utils.BytesToGB(int(nextStorageSize)),
83+		utils.BytesToGB(maxSize),
84 		(float32(nextStorageSize)/float32(maxSize))*100,
85 	)
86 
87 	return str, nil
88 }
89 
90-func (h *UploadAssetHandler) Delete(s ssh.Session, entry *utils.FileEntry) error {
91+func (h *UploadAssetHandler) Delete(s ssh.Session, entry *sendutils.FileEntry) error {
92 	user, err := futil.GetUser(s.Context())
93 	if err != nil {
94 		h.Cfg.Logger.Error("user not found in ctx", "err", err.Error())
M filehandlers/imgs/handler.go
+11, -10
 1@@ -18,7 +18,8 @@ import (
 2 	"github.com/picosh/pico/shared"
 3 	"github.com/picosh/pico/shared/storage"
 4 	"github.com/picosh/pobj"
 5-	"github.com/picosh/send/send/utils"
 6+	sendutils "github.com/picosh/send/utils"
 7+	"github.com/picosh/utils"
 8 )
 9 
10 var Space = "imgs"
11@@ -29,7 +30,7 @@ type PostMetaData struct {
12 	Cur      *db.Post
13 	Tags     []string
14 	User     *db.User
15-	*utils.FileEntry
16+	*sendutils.FileEntry
17 	FeatureFlag *db.FeatureFlag
18 }
19 
20@@ -47,7 +48,7 @@ func NewUploadImgHandler(dbpool db.DB, cfg *shared.ConfigSite, storage storage.S
21 	}
22 }
23 
24-func (h *UploadImgHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileInfo, utils.ReaderAtCloser, error) {
25+func (h *UploadImgHandler) Read(s ssh.Session, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReaderAtCloser, error) {
26 	user, err := util.GetUser(s.Context())
27 	if err != nil {
28 		return nil, nil, err
29@@ -64,7 +65,7 @@ func (h *UploadImgHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileI
30 		return nil, nil, err
31 	}
32 
33-	fileInfo := &utils.VirtualFile{
34+	fileInfo := &sendutils.VirtualFile{
35 		FName:    post.Filename,
36 		FIsDir:   false,
37 		FSize:    int64(post.FileSize),
38@@ -86,7 +87,7 @@ func (h *UploadImgHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileI
39 	return fileInfo, reader, nil
40 }
41 
42-func (h *UploadImgHandler) Write(s ssh.Session, entry *utils.FileEntry) (string, error) {
43+func (h *UploadImgHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (string, error) {
44 	logger := h.Cfg.Logger
45 	user, err := util.GetUser(s.Context())
46 	if err != nil {
47@@ -123,8 +124,8 @@ func (h *UploadImgHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
48 
49 	now := time.Now()
50 	fileSize := binary.Size(text)
51-	shasum := shared.Shasum(text)
52-	slug := shared.SanitizeFileExt(filename)
53+	shasum := utils.Shasum(text)
54+	slug := utils.SanitizeFileExt(filename)
55 
56 	nextPost := db.Post{
57 		Filename:  filename,
58@@ -184,14 +185,14 @@ func (h *UploadImgHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
59 	str := fmt.Sprintf(
60 		"%s (space: %.2f/%.2fGB, %.2f%%)",
61 		url,
62-		shared.BytesToGB(totalFileSize),
63-		shared.BytesToGB(maxSize),
64+		utils.BytesToGB(totalFileSize),
65+		utils.BytesToGB(maxSize),
66 		(float32(totalFileSize)/float32(maxSize))*100,
67 	)
68 	return str, nil
69 }
70 
71-func (h *UploadImgHandler) Delete(s ssh.Session, entry *utils.FileEntry) error {
72+func (h *UploadImgHandler) Delete(s ssh.Session, entry *sendutils.FileEntry) error {
73 	user, err := util.GetUser(s.Context())
74 	if err != nil {
75 		return err
M filehandlers/imgs/img.go
+5, -4
 1@@ -10,7 +10,8 @@ import (
 2 	"github.com/picosh/pico/db"
 3 	"github.com/picosh/pico/filehandlers/util"
 4 	"github.com/picosh/pico/shared"
 5-	"github.com/picosh/send/send/utils"
 6+	sendutils "github.com/picosh/send/utils"
 7+	"github.com/picosh/utils"
 8 )
 9 
10 func (h *UploadImgHandler) validateImg(data *PostMetaData) (bool, error) {
11@@ -29,7 +30,7 @@ func (h *UploadImgHandler) validateImg(data *PostMetaData) (bool, error) {
12 		return false, fmt.Errorf("ERROR: user (%s) has exceeded (%d bytes) max (%d bytes)", data.User.Name, totalFileSize, storageMax)
13 	}
14 
15-	if !shared.IsExtAllowed(data.Filepath, h.Cfg.AllowedExt) {
16+	if !utils.IsExtAllowed(data.Filepath, h.Cfg.AllowedExt) {
17 		extStr := strings.Join(h.Cfg.AllowedExt, ",")
18 		err := fmt.Errorf(
19 			"ERROR: (%s) invalid file, format must be (%s), skipping",
20@@ -59,8 +60,8 @@ func (h *UploadImgHandler) metaImg(data *PostMetaData) error {
21 	fname, _, err := h.Storage.PutObject(
22 		bucket,
23 		data.Filename,
24-		utils.NopReaderAtCloser(reader),
25-		&utils.FileEntry{},
26+		sendutils.NopReaderAtCloser(reader),
27+		&sendutils.FileEntry{},
28 	)
29 	if err != nil {
30 		return err
M filehandlers/post_handler.go
+10, -9
 1@@ -15,7 +15,8 @@ import (
 2 	"github.com/picosh/pico/filehandlers/util"
 3 	"github.com/picosh/pico/shared"
 4 	"github.com/picosh/pico/shared/storage"
 5-	"github.com/picosh/send/send/utils"
 6+	sendutils "github.com/picosh/send/utils"
 7+	"github.com/picosh/utils"
 8 )
 9 
10 type PostMetaData struct {
11@@ -23,7 +24,7 @@ type PostMetaData struct {
12 	Cur       *db.Post
13 	Tags      []string
14 	User      *db.User
15-	FileEntry *utils.FileEntry
16+	FileEntry *sendutils.FileEntry
17 	Aliases   []string
18 }
19 
20@@ -46,7 +47,7 @@ func NewScpPostHandler(dbpool db.DB, cfg *shared.ConfigSite, hooks ScpFileHooks,
21 	}
22 }
23 
24-func (h *ScpUploadHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileInfo, utils.ReaderAtCloser, error) {
25+func (h *ScpUploadHandler) Read(s ssh.Session, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReaderAtCloser, error) {
26 	user, err := util.GetUser(s.Context())
27 	if err != nil {
28 		return nil, nil, err
29@@ -62,19 +63,19 @@ func (h *ScpUploadHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileI
30 		return nil, nil, err
31 	}
32 
33-	fileInfo := &utils.VirtualFile{
34+	fileInfo := &sendutils.VirtualFile{
35 		FName:    post.Filename,
36 		FIsDir:   false,
37 		FSize:    int64(post.FileSize),
38 		FModTime: *post.UpdatedAt,
39 	}
40 
41-	reader := utils.NopReaderAtCloser(strings.NewReader(post.Text))
42+	reader := sendutils.NopReaderAtCloser(strings.NewReader(post.Text))
43 
44 	return fileInfo, reader, nil
45 }
46 
47-func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string, error) {
48+func (h *ScpUploadHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (string, error) {
49 	logger := h.Cfg.Logger
50 	user, err := util.GetUser(s.Context())
51 	if err != nil {
52@@ -106,9 +107,9 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
53 	}
54 
55 	now := time.Now()
56-	slug := shared.SanitizeFileExt(filename)
57+	slug := utils.SanitizeFileExt(filename)
58 	fileSize := binary.Size(origText)
59-	shasum := shared.Shasum(origText)
60+	shasum := utils.Shasum(origText)
61 
62 	nextPost := db.Post{
63 		Filename:  filename,
64@@ -261,7 +262,7 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
65 	return h.Cfg.FullPostURL(curl, user.Name, metadata.Slug), nil
66 }
67 
68-func (h *ScpUploadHandler) Delete(s ssh.Session, entry *utils.FileEntry) error {
69+func (h *ScpUploadHandler) Delete(s ssh.Session, entry *sendutils.FileEntry) error {
70 	logger := h.Cfg.Logger
71 	user, err := util.GetUser(s.Context())
72 	if err != nil {
M filehandlers/router_handler.go
+1, -1
1@@ -12,7 +12,7 @@ import (
2 	"github.com/picosh/pico/db"
3 	"github.com/picosh/pico/filehandlers/util"
4 	"github.com/picosh/pico/shared"
5-	"github.com/picosh/send/send/utils"
6+	"github.com/picosh/send/utils"
7 )
8 
9 type ReadWriteHandler interface {
M filehandlers/util/pubkey_auth.go
+2, -4
 1@@ -6,6 +6,7 @@ import (
 2 	"github.com/charmbracelet/ssh"
 3 	"github.com/picosh/pico/db"
 4 	"github.com/picosh/pico/shared"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 type SshAuthHandler struct {
 9@@ -25,10 +26,7 @@ func NewSshAuthHandler(dbpool db.DB, logger *slog.Logger, cfg *shared.ConfigSite
10 func (r *SshAuthHandler) PubkeyAuthHandler(ctx ssh.Context, key ssh.PublicKey) bool {
11 	shared.SetPublicKeyCtx(ctx, key)
12 
13-	pubkey, err := shared.KeyForKeyText(key)
14-	if err != nil {
15-		return false
16-	}
17+	pubkey := utils.KeyForKeyText(key)
18 
19 	user, err := r.DBPool.FindUserForKey(ctx.User(), pubkey)
20 	if err != nil {
M go.mod
+47, -44
  1@@ -30,15 +30,16 @@ require (
  2 	github.com/gorilla/feeds v1.1.2
  3 	github.com/lib/pq v1.10.9
  4 	github.com/microcosm-cc/bluemonday v1.0.26
  5-	github.com/minio/minio-go/v7 v7.0.70
  6+	github.com/minio/minio-go/v7 v7.0.77
  7 	github.com/mmcdole/gofeed v1.3.0
  8 	github.com/muesli/reflow v0.3.0
  9-	github.com/muesli/termenv v0.15.3-0.20240509142007-81b8f94111d5
 10+	github.com/muesli/termenv v0.15.3-0.20240912151726-82936c5ea257
 11 	github.com/neurosnap/go-exif-remove v0.0.0-20221010134343-50d1e3c35577
 12-	github.com/picosh/pobj v0.0.0-20241005185823-c92bd8ee07f8
 13-	github.com/picosh/pubsub v0.0.0-20241003170126-d92d74f10efe
 14-	github.com/picosh/send v0.0.0-20240820031602-5d3b1a4494cc
 15+	github.com/picosh/pobj v0.0.0-20241008013754-bbbfc341e2cf
 16+	github.com/picosh/pubsub v0.0.0-20241008010300-a63fd95dc8ed
 17+	github.com/picosh/send v0.0.0-20241008013240-6fdbff00f848
 18 	github.com/picosh/tunkit v0.0.0-20240709033345-8315d4f3cd0e
 19+	github.com/picosh/utils v0.0.0-20241008004349-f48b50af554b
 20 	github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
 21 	github.com/sendgrid/sendgrid-go v3.14.0+incompatible
 22 	github.com/simplesurance/go-ip-anonymizer v0.0.0-20200429124537-35a880f8e87d
 23@@ -49,7 +50,7 @@ require (
 24 	go.abhg.dev/goldmark/anchor v0.1.1
 25 	go.abhg.dev/goldmark/hashtag v0.3.1
 26 	go.abhg.dev/goldmark/toc v0.10.0
 27-	golang.org/x/crypto v0.27.0
 28+	golang.org/x/crypto v0.28.0
 29 	gopkg.in/yaml.v2 v2.4.0
 30 )
 31 
 32@@ -61,25 +62,25 @@ require (
 33 	github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
 34 	github.com/antoniomika/syncmap v1.0.0 // indirect
 35 	github.com/atotto/clipboard v0.1.4 // indirect
 36-	github.com/aws/aws-sdk-go-v2 v1.30.4 // indirect
 37-	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect
 38-	github.com/aws/aws-sdk-go-v2/config v1.27.28 // indirect
 39-	github.com/aws/aws-sdk-go-v2/credentials v1.17.28 // indirect
 40-	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 // indirect
 41-	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.11 // indirect
 42-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect
 43-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect
 44+	github.com/aws/aws-sdk-go-v2 v1.32.1 // indirect
 45+	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 // indirect
 46+	github.com/aws/aws-sdk-go-v2/config v1.27.42 // indirect
 47+	github.com/aws/aws-sdk-go-v2/credentials v1.17.40 // indirect
 48+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.16 // indirect
 49+	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.29 // indirect
 50+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.20 // indirect
 51+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.20 // indirect
 52 	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
 53-	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16 // indirect
 54-	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
 55-	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18 // indirect
 56-	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 // indirect
 57-	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16 // indirect
 58-	github.com/aws/aws-sdk-go-v2/service/s3 v1.59.0 // indirect
 59-	github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 // indirect
 60-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 // indirect
 61-	github.com/aws/aws-sdk-go-v2/service/sts v1.30.4 // indirect
 62-	github.com/aws/smithy-go v1.20.4 // indirect
 63+	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.20 // indirect
 64+	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect
 65+	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.1 // indirect
 66+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.1 // indirect
 67+	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.1 // indirect
 68+	github.com/aws/aws-sdk-go-v2/service/s3 v1.65.1 // indirect
 69+	github.com/aws/aws-sdk-go-v2/service/sso v1.24.1 // indirect
 70+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.1 // indirect
 71+	github.com/aws/aws-sdk-go-v2/service/sts v1.32.1 // indirect
 72+	github.com/aws/smithy-go v1.22.0 // indirect
 73 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
 74 	github.com/aymerick/douceur v0.2.0 // indirect
 75 	github.com/beorn7/perks v1.0.1 // indirect
 76@@ -88,7 +89,7 @@ require (
 77 	github.com/charmbracelet/log v0.4.0 // indirect
 78 	github.com/charmbracelet/x/ansi v0.3.2 // indirect
 79 	github.com/charmbracelet/x/conpty v0.1.0 // indirect
 80-	github.com/charmbracelet/x/errors v0.0.0-20240919170804-a4978c8e603a // indirect
 81+	github.com/charmbracelet/x/errors v0.0.0-20241007193646-7cc13b2883e3 // indirect
 82 	github.com/charmbracelet/x/input v0.2.0 // indirect
 83 	github.com/charmbracelet/x/term v0.2.0 // indirect
 84 	github.com/charmbracelet/x/termios v0.1.0 // indirect
 85@@ -109,6 +110,7 @@ require (
 86 	github.com/gdamore/encoding v1.0.1 // indirect
 87 	github.com/gdamore/tcell/v2 v2.7.4 // indirect
 88 	github.com/go-errors/errors v1.5.1 // indirect
 89+	github.com/go-ini/ini v1.67.0 // indirect
 90 	github.com/go-logfmt/logfmt v0.6.0 // indirect
 91 	github.com/go-ole/go-ole v1.3.0 // indirect
 92 	github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect
 93@@ -120,16 +122,16 @@ require (
 94 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 95 	github.com/gorilla/css v1.0.1 // indirect
 96 	github.com/json-iterator/go v1.1.12 // indirect
 97-	github.com/klauspost/compress v1.17.8 // indirect
 98-	github.com/klauspost/cpuid/v2 v2.2.7 // indirect
 99+	github.com/klauspost/compress v1.17.10 // indirect
100+	github.com/klauspost/cpuid/v2 v2.2.8 // indirect
101 	github.com/kr/fs v0.1.0 // indirect
102 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
103-	github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
104+	github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
105 	github.com/mattn/go-isatty v0.0.20 // indirect
106 	github.com/mattn/go-localereader v0.0.1 // indirect
107 	github.com/mattn/go-runewidth v0.0.16 // indirect
108 	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
109-	github.com/minio/madmin-go/v3 v3.0.54 // indirect
110+	github.com/minio/madmin-go/v3 v3.0.70 // indirect
111 	github.com/minio/md5-simd v1.1.2 // indirect
112 	github.com/mmcdole/goxpp v1.1.1 // indirect
113 	github.com/mmcloughlin/md4 v0.1.2 // indirect
114@@ -137,39 +139,40 @@ require (
115 	github.com/modern-go/reflect2 v1.0.2 // indirect
116 	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
117 	github.com/muesli/cancelreader v0.2.2 // indirect
118+	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
119 	github.com/neurosnap/go-jpeg-image-structure v0.0.0-20221010133817-70b1c1ff679e // indirect
120 	github.com/olekukonko/tablewriter v0.0.5 // indirect
121-	github.com/philhofer/fwd v1.1.2 // indirect
122+	github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
123 	github.com/picosh/go-rsync-receiver v0.0.0-20240709135253-1daf4b12a9fc // indirect
124 	github.com/pkg/sftp v1.13.6 // indirect
125 	github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
126-	github.com/prometheus/client_golang v1.19.1 // indirect
127+	github.com/prometheus/client_golang v1.20.4 // indirect
128 	github.com/prometheus/client_model v0.6.1 // indirect
129-	github.com/prometheus/common v0.54.0 // indirect
130+	github.com/prometheus/common v0.60.0 // indirect
131 	github.com/prometheus/procfs v0.15.1 // indirect
132-	github.com/prometheus/prom2json v1.3.3 // indirect
133+	github.com/prometheus/prom2json v1.4.1 // indirect
134+	github.com/prometheus/prometheus v0.54.1 // indirect
135 	github.com/rivo/uniseg v0.4.7 // indirect
136 	github.com/rogpeppe/go-internal v1.11.0 // indirect
137-	github.com/rs/xid v1.5.0 // indirect
138-	github.com/safchain/ethtool v0.3.0 // indirect
139+	github.com/rs/xid v1.6.0 // indirect
140+	github.com/safchain/ethtool v0.4.1 // indirect
141 	github.com/secure-io/sio-go v0.3.1 // indirect
142 	github.com/sendgrid/rest v2.6.9+incompatible // indirect
143 	github.com/shirou/gopsutil/v3 v3.24.5 // indirect
144 	github.com/shoenig/go-m1cpu v0.1.6 // indirect
145-	github.com/tinylib/msgp v1.1.9 // indirect
146+	github.com/tinylib/msgp v1.2.2 // indirect
147 	github.com/tklauser/go-sysconf v0.3.14 // indirect
148-	github.com/tklauser/numcpus v0.8.0 // indirect
149+	github.com/tklauser/numcpus v0.9.0 // indirect
150 	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
151 	github.com/yuin/goldmark-emoji v1.0.2 // indirect
152 	github.com/yusufpapurcu/wmi v1.2.4 // indirect
153-	golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
154-	golang.org/x/net v0.25.0 // indirect
155+	golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 // indirect
156+	golang.org/x/net v0.30.0 // indirect
157 	golang.org/x/sync v0.8.0 // indirect
158-	golang.org/x/sys v0.25.0 // indirect
159-	golang.org/x/term v0.24.0 // indirect
160-	golang.org/x/text v0.18.0 // indirect
161+	golang.org/x/sys v0.26.0 // indirect
162+	golang.org/x/term v0.25.0 // indirect
163+	golang.org/x/text v0.19.0 // indirect
164 	golang.org/x/time v0.6.0 // indirect
165-	google.golang.org/protobuf v1.34.1 // indirect
166-	gopkg.in/ini.v1 v1.67.0 // indirect
167+	google.golang.org/protobuf v1.35.1 // indirect
168 	mvdan.cc/xurls/v2 v2.5.0 // indirect
169 )
M go.sum
+99, -93
  1@@ -23,44 +23,44 @@ github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhP
  2 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
  3 github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
  4 github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
  5-github.com/aws/aws-sdk-go-v2 v1.30.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8=
  6-github.com/aws/aws-sdk-go-v2 v1.30.4/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
  7-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 h1:70PVAiL15/aBMh5LThwgXdSQorVr91L127ttckI9QQU=
  8-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4/go.mod h1:/MQxMqci8tlqDH+pjmoLu1i0tbWCUP1hhyMRuFxpQCw=
  9-github.com/aws/aws-sdk-go-v2/config v1.27.28 h1:OTxWGW/91C61QlneCtnD62NLb4W616/NM1jA8LhJqbg=
 10-github.com/aws/aws-sdk-go-v2/config v1.27.28/go.mod h1:uzVRVtJSU5EFv6Fu82AoVFKozJi2ZCY6WRCXj06rbvs=
 11-github.com/aws/aws-sdk-go-v2/credentials v1.17.28 h1:m8+AHY/ND8CMHJnPoH7PJIRakWGa4gbfbxuY9TGTUXM=
 12-github.com/aws/aws-sdk-go-v2/credentials v1.17.28/go.mod h1:6TF7dSc78ehD1SL6KpRIPKMA1GyyWflIkjqg+qmf4+c=
 13-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 h1:yjwoSyDZF8Jth+mUk5lSPJCkMC0lMy6FaCD51jm6ayE=
 14-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12/go.mod h1:fuR57fAgMk7ot3WcNQfb6rSEn+SUffl7ri+aa8uKysI=
 15-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.11 h1:FEDZD/Axt5tKSkPAs967KZ++MkvYdBqr0a+cetRbjLM=
 16-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.11/go.mod h1:dvlsbA32KfvCzqwTiX7maABgFek2RyUuYEJ3kyn/PmQ=
 17-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 h1:TNyt/+X43KJ9IJJMjKfa3bNTiZbUP7DeCxfbTROESwY=
 18-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16/go.mod h1:2DwJF39FlNAUiX5pAc0UNeiz16lK2t7IaFcm0LFHEgc=
 19-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 h1:jYfy8UPmd+6kJW5YhY0L1/KftReOGxI/4NtVSTh9O/I=
 20-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16/go.mod h1:7ZfEPZxkW42Afq4uQB8H2E2e6ebh6mXTueEpYzjCzcs=
 21+github.com/aws/aws-sdk-go-v2 v1.32.1 h1:8WuZ43ytA+TV6QEPT/R23mr7pWyI7bSSiEHdt9BS2Pw=
 22+github.com/aws/aws-sdk-go-v2 v1.32.1/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo=
 23+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 h1:pT3hpW0cOHRJx8Y0DfJUEQuqPild8jRGmSFmBgvydr0=
 24+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6/go.mod h1:j/I2++U0xX+cr44QjHay4Cvxj6FUbnxrgmqN3H1jTZA=
 25+github.com/aws/aws-sdk-go-v2/config v1.27.42 h1:Zsy9coUPuOsCWkjTvHpl2/DB9bptXtv7WeNPxvFr87s=
 26+github.com/aws/aws-sdk-go-v2/config v1.27.42/go.mod h1:FGASs+PuJM2EY+8rt8qyQKLPbbX/S5oY+6WzJ/KE7ko=
 27+github.com/aws/aws-sdk-go-v2/credentials v1.17.40 h1:RjnlA7t0p/IamxAM7FUJ5uS13Vszh4sjVGvsx91tGro=
 28+github.com/aws/aws-sdk-go-v2/credentials v1.17.40/go.mod h1:dgpdnSs1Bp/atS6vLlW83h9xZPP+uSPB/27dFSgC1BM=
 29+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.16 h1:fwrer1pJeaiia0CcOfWVbZxvj9Adc7rsuaMTwPR0DIA=
 30+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.16/go.mod h1:XyEwwp8XI4zMar7MTnJ0Sk7qY/9aN8Hp929XhuX5SF8=
 31+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.29 h1:eyeHfJ9FAb7sd5ODTkjrfot3gS0Ln4vn/18l7zZMCik=
 32+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.29/go.mod h1:JpzRPe12SjlOmuqgi+/5RmgfbsWzDYdfxe3Abrk2kW8=
 33+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.20 h1:OErdlGnt+hg3tTwGYAlKvFkKVUo/TXkoHcxDxuhYYU8=
 34+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.20/go.mod h1:HsPfuL5gs+407ByRXBMgpYoyrV1sgMrzd18yMXQHJpo=
 35+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.20 h1:822cE1CYSwY/EZnErlF46pyynuxvf1p+VydHRQW+XNs=
 36+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.20/go.mod h1:79/Tn7H7hYC5Gjz6fbnOV4OeBpkao7E8Tv95RO72pMM=
 37 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
 38 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
 39-github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16 h1:mimdLQkIX1zr8GIPY1ZtALdBQGxcASiBd2MOp8m/dMc=
 40-github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16/go.mod h1:YHk6owoSwrIsok+cAH9PENCOGoH5PU2EllX4vLtSrsY=
 41-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI=
 42-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc=
 43-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18 h1:GckUnpm4EJOAio1c8o25a+b3lVfwVzC9gnSBqiiNmZM=
 44-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18/go.mod h1:Br6+bxfG33Dk3ynmkhsW2Z/t9D4+lRqdLDNCKi85w0U=
 45-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 h1:tJ5RnkHCiSH0jyd6gROjlJtNwov0eGYNz8s8nFcR0jQ=
 46-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18/go.mod h1:++NHzT+nAF7ZPrHPsA+ENvsXkOO8wEu+C6RXltAG4/c=
 47-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16 h1:jg16PhLPUiHIj8zYIW6bqzeQSuHVEiWnGA0Brz5Xv2I=
 48-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16/go.mod h1:Uyk1zE1VVdsHSU7096h/rwnXDzOzYQVl+FNPhPw7ShY=
 49-github.com/aws/aws-sdk-go-v2/service/s3 v1.59.0 h1:Cso4Ev/XauMVsbwdhYEoxg8rxZWw43CFqqaPB5w3W2c=
 50-github.com/aws/aws-sdk-go-v2/service/s3 v1.59.0/go.mod h1:BSPI0EfnYUuNHPS0uqIo5VrRwzie+Fp+YhQOUs16sKI=
 51-github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 h1:zCsFCKvbj25i7p1u94imVoO447I/sFv8qq+lGJhRN0c=
 52-github.com/aws/aws-sdk-go-v2/service/sso v1.22.5/go.mod h1:ZeDX1SnKsVlejeuz41GiajjZpRSWR7/42q/EyA/QEiM=
 53-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 h1:SKvPgvdvmiTWoi0GAJ7AsJfOz3ngVkD/ERbs5pUnHNI=
 54-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5/go.mod h1:20sz31hv/WsPa3HhU3hfrIet2kxM4Pe0r20eBZ20Tac=
 55-github.com/aws/aws-sdk-go-v2/service/sts v1.30.4 h1:iAckBT2OeEK/kBDyN/jDtpEExhjeeA/Im2q4X0rJZT8=
 56-github.com/aws/aws-sdk-go-v2/service/sts v1.30.4/go.mod h1:vmSqFK+BVIwVpDAGZB3CoCXHzurt4qBE8lf+I/kRTh0=
 57-github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4=
 58-github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
 59+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.20 h1:HO5UCCkLmeWkJZHLvLDfylKv8ca28XLAX3HojZz2shI=
 60+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.20/go.mod h1:IO0HUM6Ouk/s7Rx3hiLtFU3mc+9OJFFygjsaxFBhAbk=
 61+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g=
 62+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ=
 63+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.1 h1:UeW3Ul28hkKvB3beWImBvO7U62tSmapxaqk8sX9SMCU=
 64+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.1/go.mod h1:TER/1DuTxSN6RFQpk3xfD9hK4A1gQ7ainfkwHV3LPtU=
 65+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.1 h1:5vBMBTakOvtd8aNaicswcrr9qqCYUlasuzyoU6/0g8I=
 66+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.1/go.mod h1:WSUbDa5qdg05Q558KXx2Scb+EDvOPXT9gfET0fyrJSk=
 67+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.1 h1:T6oOYbNQ+iqdtG1/mTJvMBg/YFyHR8Z8URyG3qK+Anc=
 68+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.1/go.mod h1:25CEM6c1e2vyLcr3fPritPsdsoMwNAOc9//M1QAwtDk=
 69+github.com/aws/aws-sdk-go-v2/service/s3 v1.65.1 h1:HQR79P0F0C2YQOaS2Z+90YK9DH22z9D6Neplaj0yuy4=
 70+github.com/aws/aws-sdk-go-v2/service/s3 v1.65.1/go.mod h1:xYVl5BX9Ws7+ZM58b3w0kq36TR1Dgw2OMkjSr6YTWXg=
 71+github.com/aws/aws-sdk-go-v2/service/sso v1.24.1 h1:aAIr0WhAgvKrxZtkBqne87Gjmd7/lJVTFkR2l2yuhL8=
 72+github.com/aws/aws-sdk-go-v2/service/sso v1.24.1/go.mod h1:8XhxGMWUfikJuginPQl5SGZ0LSJuNX3TCEQmFWZwHTM=
 73+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.1 h1:J6kIsIkgFOaU6aKjigXJoue1XEHtKIIrpSh4vKdmRTs=
 74+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.1/go.mod h1:2V2JLP7tXOmUbL3Hd1ojq+774t2KUAEQ35//shoNEL0=
 75+github.com/aws/aws-sdk-go-v2/service/sts v1.32.1 h1:q76Ig4OaJzVJGNUSGO3wjSTBS94g+EhHIbpY9rPvkxs=
 76+github.com/aws/aws-sdk-go-v2/service/sts v1.32.1/go.mod h1:664dajZ7uS7JMUMUG0R5bWbtN97KECNCVdFDdQ6Ipu8=
 77+github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM=
 78+github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
 79 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
 80 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
 81 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
 82@@ -91,8 +91,8 @@ github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyf
 83 github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
 84 github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
 85 github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
 86-github.com/charmbracelet/x/errors v0.0.0-20240919170804-a4978c8e603a h1:IlWNrDYRP6lyttqsFHDhmo8NXggMwBuhvvCP+Wmb2a8=
 87-github.com/charmbracelet/x/errors v0.0.0-20240919170804-a4978c8e603a/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
 88+github.com/charmbracelet/x/errors v0.0.0-20241007193646-7cc13b2883e3 h1:nsBhzPXBqeXEGZ9ztveSIPdf790BcDikbaEH3vMglH4=
 89+github.com/charmbracelet/x/errors v0.0.0-20241007193646-7cc13b2883e3/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
 90 github.com/charmbracelet/x/input v0.2.0 h1:1Sv+y/flcqUfUH2PXNIDKDIdT2G8smOnGOgawqhwy8A=
 91 github.com/charmbracelet/x/input v0.2.0/go.mod h1:KUSFIS6uQymtnr5lHVSOK9j8RvwTD4YHnWnzJUYnd/M=
 92 github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
 93@@ -102,8 +102,9 @@ github.com/charmbracelet/x/termios v0.1.0/go.mod h1:H/EVv/KRnrYjz+fCYa9bsKdqF3S8
 94 github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
 95 github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
 96 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 97-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 98 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 99+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
100+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
101 github.com/delthas/go-libnp v0.0.0-20221222161248-0e45ece1f878 h1:v8W8eW7eb2bHFXBA80UKcoe0TvEu46NlTHSDRvgAbMU=
102 github.com/delthas/go-libnp v0.0.0-20221222161248-0e45ece1f878/go.mod h1:aGVXnhWpDlt5U4SphG97o1gszctZKvBTXy320E8Buw4=
103 github.com/delthas/go-localeinfo v0.0.0-20221116001557-686a1e185118 h1:Xzf9ra1QRJXD62gwudjI2iBq7x9CusvHd83Dg2OnUmE=
104@@ -152,6 +153,8 @@ github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWE
105 github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
106 github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
107 github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
108+github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
109+github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
110 github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
111 github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
112 github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
113@@ -189,23 +192,25 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSo
114 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
115 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
116 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
117-github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
118-github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
119+github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
120+github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
121 github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
122-github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
123-github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
124+github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
125+github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
126 github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
127 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
128 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
129 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
130 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
131 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
132+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
133+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
134 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
135 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
136 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
137 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
138-github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI=
139-github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
140+github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
141+github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
142 github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
143 github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
144 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
145@@ -223,12 +228,12 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk
146 github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
147 github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
148 github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
149-github.com/minio/madmin-go/v3 v3.0.54 h1:Mwp6JYSY9GxbrUoMMij2rTzSBaCps575dlsuKO8D3yk=
150-github.com/minio/madmin-go/v3 v3.0.54/go.mod h1:IFAwr0XMrdsLovxAdCcuq/eoL4nRuMVQQv0iubJANQw=
151+github.com/minio/madmin-go/v3 v3.0.70 h1:zrFCXLcV6PR74JC0yytK4Dk2qsaCV8kXQoPTvcusR2k=
152+github.com/minio/madmin-go/v3 v3.0.70/go.mod h1:TOTc96ZkMknNhl+ReO/V68bQfgRGfH+8iy7YaDzHdXA=
153 github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
154 github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
155-github.com/minio/minio-go/v7 v7.0.70 h1:1u9NtMgfK1U42kUxcsl5v0yj6TEOPR497OAQxpJnn2g=
156-github.com/minio/minio-go/v7 v7.0.70/go.mod h1:4yBA8v80xGA30cfM3fz0DKYMXunWl/AV/6tWEs9ryzo=
157+github.com/minio/minio-go/v7 v7.0.77 h1:GaGghJRg9nwDVlNbwYjSDJT1rqltQkBFDsypWX1v3Bw=
158+github.com/minio/minio-go/v7 v7.0.77/go.mod h1:AVM3IUN6WwKzmwBxVdjzhH8xq+f57JSbbvzqvUzR6eg=
159 github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4=
160 github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE=
161 github.com/mmcdole/goxpp v1.1.1 h1:RGIX+D6iQRIunGHrKqnA2+700XMCnNv0bAOOv5MUhx8=
162@@ -246,32 +251,32 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
163 github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
164 github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
165 github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
166-github.com/muesli/termenv v0.15.3-0.20240509142007-81b8f94111d5 h1:NiONcKK0EV5gUZcnCiPMORaZA0eBDc+Fgepl9xl4lZ8=
167-github.com/muesli/termenv v0.15.3-0.20240509142007-81b8f94111d5/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
168+github.com/muesli/termenv v0.15.3-0.20240912151726-82936c5ea257 h1:RNw/zu+CJemcRlDFPjElZUbY2UlI/MA2B3I6PM3Isiw=
169+github.com/muesli/termenv v0.15.3-0.20240912151726-82936c5ea257/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
170+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
171+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
172 github.com/neurosnap/go-exif-remove v0.0.0-20221010134343-50d1e3c35577 h1:hVmVNttSLNloGsbFKVXAUHonXTd8KKrv30U/8UkloKI=
173 github.com/neurosnap/go-exif-remove v0.0.0-20221010134343-50d1e3c35577/go.mod h1:G3Cu1AW+dmRLDFpOi8eUAfc3cGoRHUjTkGjeRcndgl4=
174 github.com/neurosnap/go-jpeg-image-structure v0.0.0-20221010133817-70b1c1ff679e h1:76Dng5ms0fR+26doKZAvNqhi2UPfnLxGfPIDEr+BBlM=
175 github.com/neurosnap/go-jpeg-image-structure v0.0.0-20221010133817-70b1c1ff679e/go.mod h1:nZBDA7+RD63GDJwjZmxhxac65MJqiCIHUUUvdYOsFkk=
176 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
177 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
178-github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
179-github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
180+github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
181+github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
182 github.com/picosh/go-rsync-receiver v0.0.0-20240709135253-1daf4b12a9fc h1:bvcsoOvaNHPquFnRkdraEo7+8t6bW7nWEhlALnwZPdI=
183 github.com/picosh/go-rsync-receiver v0.0.0-20240709135253-1daf4b12a9fc/go.mod h1:i0iR3W4GSm1PuvVxB9OH32E5jP+CYkVb2NQSe0JCtlo=
184-github.com/picosh/pobj v0.0.0-20240709135546-27097077b26a h1:Cr1xODiyd/SjjBRtYA9VX6Do3D+w+DansQzkb4NGeyA=
185-github.com/picosh/pobj v0.0.0-20240709135546-27097077b26a/go.mod h1:VIkR1MZBvxSK2OO47jikxikAO/sKb/vTmXX5ZuYTIvo=
186-github.com/picosh/pobj v0.0.0-20241005184424-366421c762d7 h1:QiKWMuxqhNKfOacqfZl0fJn7Oc53hIoB1WH7eluOqs0=
187-github.com/picosh/pobj v0.0.0-20241005184424-366421c762d7/go.mod h1:tPv/ydMnuXlGVpWUMvBlcWlyn/xu9PsVPdncYibweWY=
188-github.com/picosh/pobj v0.0.0-20241005185823-c92bd8ee07f8 h1:M6GjB28u/sThE3gyKx4V2iqrokdMJAr1pQ41Q3mK04I=
189-github.com/picosh/pobj v0.0.0-20241005185823-c92bd8ee07f8/go.mod h1:tPv/ydMnuXlGVpWUMvBlcWlyn/xu9PsVPdncYibweWY=
190-github.com/picosh/pubsub v0.0.0-20241003170126-d92d74f10efe h1:NQA2eXxqFPjVr/8DX073Vap5kMnJk3/AAAgt/jz8cyc=
191-github.com/picosh/pubsub v0.0.0-20241003170126-d92d74f10efe/go.mod h1:gWhwrStKWJNzp9i44Bc3YQmnC+pIKvI5dYWm1GRHJac=
192-github.com/picosh/send v0.0.0-20240820031602-5d3b1a4494cc h1:IIsJuAFG2ju3cygKVKTIsYYZf21q5S3Dr1H4fGbfgJg=
193-github.com/picosh/send v0.0.0-20240820031602-5d3b1a4494cc/go.mod h1:RAgLDK3LrDK6pNeXtU9tjo28obl5DxShcTUk2nm/KCM=
194+github.com/picosh/pobj v0.0.0-20241008013754-bbbfc341e2cf h1:Ul+LuTVXRimpIneOHez05k7VOV/lDVw37I18rceEplw=
195+github.com/picosh/pobj v0.0.0-20241008013754-bbbfc341e2cf/go.mod h1:cF+eAl4G1vU+WOD8cYCKaxokHo6MWmbR8J4/SJnvESg=
196+github.com/picosh/pubsub v0.0.0-20241008010300-a63fd95dc8ed h1:aBJeQoLvq/V3hX6bgWjuuTmGzgbPNYuuwaCWU4aSJcU=
197+github.com/picosh/pubsub v0.0.0-20241008010300-a63fd95dc8ed/go.mod h1:ajolgob5MxlHdp5HllF7u3rTlCgER4InqfP7M/xl6HQ=
198+github.com/picosh/send v0.0.0-20241008013240-6fdbff00f848 h1:VWbjNNOqpJ8AB3zdw+M5+XC/SINooWLGi6WCozKwt1o=
199+github.com/picosh/send v0.0.0-20241008013240-6fdbff00f848/go.mod h1:RAgLDK3LrDK6pNeXtU9tjo28obl5DxShcTUk2nm/KCM=
200 github.com/picosh/senpai v0.0.0-20240503200611-af89e73973b0 h1:pBRIbiCj7K6rGELijb//dYhyCo8A3fvxW5dijrJVtjs=
201 github.com/picosh/senpai v0.0.0-20240503200611-af89e73973b0/go.mod h1:QaBDtybFC5gz7EG/9c3bgzuyW7W5W2rYLFZxWNuWk3Q=
202 github.com/picosh/tunkit v0.0.0-20240709033345-8315d4f3cd0e h1:3rNSjBJ6VlvngWF58V/z0fPLH7WyzKpSboC6YznECgw=
203 github.com/picosh/tunkit v0.0.0-20240709033345-8315d4f3cd0e/go.mod h1:UrDH/VCIc1wg/L6iY2zSYt4TiGw+25GsKSnkVkU40Dw=
204+github.com/picosh/utils v0.0.0-20241008004349-f48b50af554b h1:PvWk8Y7JhC1bK4Ns7FUFfcvi+BGZ+K07wTA2VDTmfDQ=
205+github.com/picosh/utils v0.0.0-20241008004349-f48b50af554b/go.mod h1:ftrp1FjbKK/mFnBAYGymA1QEtPlkA0+lWkPI5h0HKt4=
206 github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
207 github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
208 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
209@@ -279,16 +284,18 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
210 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
211 github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
212 github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
213-github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
214-github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
215+github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI=
216+github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
217 github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
218 github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
219-github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8=
220-github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
221+github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA=
222+github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
223 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
224 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
225-github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcETyaUgo=
226-github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc=
227+github.com/prometheus/prom2json v1.4.1 h1:7McxdrHgPEOtMwWjkKtd0v5AhpR2Q6QAnlHKVxq0+tQ=
228+github.com/prometheus/prom2json v1.4.1/go.mod h1:CzOQykSKFxXuC7ELUZHOHQvwKesQ3eN0p2PWLhFitQM=
229+github.com/prometheus/prometheus v0.54.1 h1:vKuwQNjnYN2/mDoWfHXDhAsz/68q/dQDb+YbcEqU7MQ=
230+github.com/prometheus/prometheus v0.54.1/go.mod h1:xlLByHhk2g3ycakQGrMaU8K7OySZx98BzeCR99991NY=
231 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
232 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
233 github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
234@@ -297,12 +304,12 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
235 github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
236 github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
237 github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
238-github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
239-github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
240+github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
241+github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
242 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
243 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
244-github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
245-github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
246+github.com/safchain/ethtool v0.4.1 h1:S6mEleTADqgynileXoiapt/nKnatyR6bmIHoF+h2ADo=
247+github.com/safchain/ethtool v0.4.1/go.mod h1:XLLnZmy4OCRTkksP/UiMjij96YmIsBfmBQcs7H6tA48=
248 github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
249 github.com/secure-io/sio-go v0.3.1 h1:dNvY9awjabXTYGsTF1PiCySl9Ltofk9GA3VdWlo7rRc=
250 github.com/secure-io/sio-go v0.3.1/go.mod h1:+xbkjDzPjwh4Axd07pRKSNriS9SCiYksWnZqdnfpQxs=
251@@ -327,12 +334,12 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
252 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
253 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
254 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
255-github.com/tinylib/msgp v1.1.9 h1:SHf3yoO2sGA0veCJeCBYLHuttAVFHGm2RHgNodW7wQU=
256-github.com/tinylib/msgp v1.1.9/go.mod h1:BCXGB54lDD8qUEPmiG0cQQUANC4IUQyB2ItS2UDlO/k=
257+github.com/tinylib/msgp v1.2.2 h1:iHiBE1tJQwFI740SPEPkGE8cfhNfrqOYRlH450BnC/4=
258+github.com/tinylib/msgp v1.2.2/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
259 github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
260 github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
261-github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
262-github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
263+github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
264+github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
265 github.com/x-way/crawlerdetect v0.2.21 h1:LORs0nEy+MWUsC3XvKf00hXyO7drB5w/hlGB8bztXbI=
266 github.com/x-way/crawlerdetect v0.2.21/go.mod h1:DVupfue81iupuoUmFjIyDUqPqGaJhtZfYQDWoP1ZUR4=
267 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
268@@ -361,10 +368,10 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh
269 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
270 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
271 golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
272-golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
273-golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
274-golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
275-golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
276+golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
277+golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
278+golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 h1:1wqE9dj9NpSm04INVsJhhEUzhuDVjbcyKH91sVyPATw=
279+golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
280 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
281 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
282 golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
283@@ -382,8 +389,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
284 golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
285 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
286 golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
287-golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
288-golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
289+golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
290+golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
291 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
292 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
293 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
294@@ -407,8 +414,9 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
295 golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
296 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
297 golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
298-golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
299-golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
300+golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
301+golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
302+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
303 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
304 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
305 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
306@@ -416,8 +424,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
307 golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
308 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
309 golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
310-golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
311-golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
312+golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
313+golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
314 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
315 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
316 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
317@@ -425,8 +433,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
318 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
319 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
320 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
321-golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
322-golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
323+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
324+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
325 golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
326 golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
327 golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
328@@ -435,13 +443,11 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
329 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
330 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
331 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
332-google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
333-google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
334+google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
335+google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
336 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
337 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
338 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
339-gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
340-gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
341 gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
342 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
343 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
M imgs/api.go
+2, -1
 1@@ -17,6 +17,7 @@ import (
 2 	"github.com/picosh/pico/pgs"
 3 	"github.com/picosh/pico/shared"
 4 	"github.com/picosh/pico/shared/storage"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 type PostPageData struct {
 9@@ -219,7 +220,7 @@ func ImgRequest(w http.ResponseWriter, r *http.Request) {
10 		if ext == fext {
11 			// users might add the file extension when requesting an image
12 			// but we want to remove that
13-			slug = shared.SanitizeFileExt(slug)
14+			slug = utils.SanitizeFileExt(slug)
15 			break
16 		}
17 	}
M imgs/cli.go
+7, -7
 1@@ -14,21 +14,21 @@ import (
 2 	"github.com/charmbracelet/wish"
 3 	"github.com/google/uuid"
 4 	"github.com/picosh/pico/db"
 5-	"github.com/picosh/pico/shared"
 6 	"github.com/picosh/pico/shared/storage"
 7 	"github.com/picosh/pico/tui/common"
 8 	sst "github.com/picosh/pobj/storage"
 9 	psub "github.com/picosh/pubsub"
10-	"github.com/picosh/send/send/utils"
11+	sendutils "github.com/picosh/send/utils"
12+	"github.com/picosh/utils"
13 )
14 
15 func getUser(s ssh.Session, dbpool db.DB) (*db.User, error) {
16-	var err error
17-	key, err := shared.KeyText(s)
18-	if err != nil {
19+	if s.PublicKey() == nil {
20 		return nil, fmt.Errorf("key not found")
21 	}
22 
23+	key := utils.KeyForKeyText(s.PublicKey())
24+
25 	user, err := dbpool.FindUserForKey(s.User(), key)
26 	if err != nil {
27 		return nil, err
28@@ -60,7 +60,7 @@ func flagCheck(cmd *flag.FlagSet, posArg string, cmdArgs []string) bool {
29 
30 type Cmd struct {
31 	User        *db.User
32-	Session     shared.CmdSession
33+	Session     utils.CmdSession
34 	Log         *slog.Logger
35 	Dbpool      db.DB
36 	Write       bool
37@@ -201,7 +201,7 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
38 		return func(sesh ssh.Session) {
39 			user, err := getUser(sesh, dbpool)
40 			if err != nil {
41-				utils.ErrorHandler(sesh, err)
42+				sendutils.ErrorHandler(sesh, err)
43 				return
44 			}
45 
M imgs/config.go
+10, -9
 1@@ -2,18 +2,19 @@ package imgs
 2 
 3 import (
 4 	"github.com/picosh/pico/shared"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 func NewConfigSite() *shared.ConfigSite {
 9-	debug := shared.GetEnv("IMGS_DEBUG", "0")
10-	domain := shared.GetEnv("IMGS_DOMAIN", "prose.sh")
11-	port := shared.GetEnv("IMGS_WEB_PORT", "3000")
12-	protocol := shared.GetEnv("IMGS_PROTOCOL", "https")
13-	storageDir := shared.GetEnv("IMGS_STORAGE_DIR", ".storage")
14-	minioURL := shared.GetEnv("MINIO_URL", "")
15-	minioUser := shared.GetEnv("MINIO_ROOT_USER", "")
16-	minioPass := shared.GetEnv("MINIO_ROOT_PASSWORD", "")
17-	dbURL := shared.GetEnv("DATABASE_URL", "")
18+	debug := utils.GetEnv("IMGS_DEBUG", "0")
19+	domain := utils.GetEnv("IMGS_DOMAIN", "prose.sh")
20+	port := utils.GetEnv("IMGS_WEB_PORT", "3000")
21+	protocol := utils.GetEnv("IMGS_PROTOCOL", "https")
22+	storageDir := utils.GetEnv("IMGS_STORAGE_DIR", ".storage")
23+	minioURL := utils.GetEnv("MINIO_URL", "")
24+	minioUser := utils.GetEnv("MINIO_ROOT_USER", "")
25+	minioPass := utils.GetEnv("MINIO_ROOT_PASSWORD", "")
26+	dbURL := utils.GetEnv("DATABASE_URL", "")
27 
28 	cfg := shared.ConfigSite{
29 		Debug:      debug == "1",
M imgs/ssh.go
+6, -9
 1@@ -27,6 +27,7 @@ import (
 2 	"github.com/picosh/pico/shared/storage"
 3 	psub "github.com/picosh/pubsub"
 4 	"github.com/picosh/tunkit"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 type ctxUserKey struct{}
 9@@ -44,11 +45,7 @@ func setUserCtx(ctx ssh.Context, user *db.User) {
10 
11 func AuthHandler(dbh db.DB, log *slog.Logger) func(ssh.Context, ssh.PublicKey) bool {
12 	return func(ctx ssh.Context, key ssh.PublicKey) bool {
13-		kk, err := shared.KeyForKeyText(key)
14-		if err != nil {
15-			log.Error("cannot get pubkey", "err", err)
16-			return false
17-		}
18+		kk := utils.KeyForKeyText(key)
19 
20 		user, err := dbh.FindUserForKey("", kk)
21 		if err != nil {
22@@ -263,10 +260,10 @@ func StartSshServer() {
23 		port = "2222"
24 	}
25 	dbUrl := os.Getenv("DATABASE_URL")
26-	registryUrl := shared.GetEnv("REGISTRY_URL", "0.0.0.0:5000")
27-	minioUrl := shared.GetEnv("MINIO_URL", "http://0.0.0.0:9000")
28-	minioUser := shared.GetEnv("MINIO_ROOT_USER", "")
29-	minioPass := shared.GetEnv("MINIO_ROOT_PASSWORD", "")
30+	registryUrl := utils.GetEnv("REGISTRY_URL", "0.0.0.0:5000")
31+	minioUrl := utils.GetEnv("MINIO_URL", "http://0.0.0.0:9000")
32+	minioUser := utils.GetEnv("MINIO_ROOT_USER", "")
33+	minioPass := utils.GetEnv("MINIO_ROOT_PASSWORD", "")
34 
35 	logger := shared.CreateLogger("imgs")
36 	logger.Info("bootup", "registry", registryUrl, "minio", minioUrl)
M pastes/api.go
+2, -1
 1@@ -12,6 +12,7 @@ import (
 2 	"github.com/picosh/pico/db/postgres"
 3 	"github.com/picosh/pico/shared"
 4 	"github.com/picosh/pico/shared/storage"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 type PageData struct {
 9@@ -123,7 +124,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
10 			Title:          post.Filename,
11 			PublishAt:      post.PublishAt.Format(time.DateOnly),
12 			PublishAtISO:   post.PublishAt.Format(time.RFC3339),
13-			UpdatedTimeAgo: shared.TimeAgo(post.UpdatedAt),
14+			UpdatedTimeAgo: utils.TimeAgo(post.UpdatedAt),
15 			UpdatedAtISO:   post.UpdatedAt.Format(time.RFC3339),
16 		}
17 		postCollection = append(postCollection, p)
M pastes/config.go
+10, -9
 1@@ -2,18 +2,19 @@ package pastes
 2 
 3 import (
 4 	"github.com/picosh/pico/shared"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 func NewConfigSite() *shared.ConfigSite {
 9-	debug := shared.GetEnv("PASTES_DEBUG", "0")
10-	domain := shared.GetEnv("PASTES_DOMAIN", "pastes.sh")
11-	port := shared.GetEnv("PASTES_WEB_PORT", "3000")
12-	dbURL := shared.GetEnv("DATABASE_URL", "")
13-	protocol := shared.GetEnv("PASTES_PROTOCOL", "https")
14-	storageDir := shared.GetEnv("IMGS_STORAGE_DIR", ".storage")
15-	minioURL := shared.GetEnv("MINIO_URL", "")
16-	minioUser := shared.GetEnv("MINIO_ROOT_USER", "")
17-	minioPass := shared.GetEnv("MINIO_ROOT_PASSWORD", "")
18+	debug := utils.GetEnv("PASTES_DEBUG", "0")
19+	domain := utils.GetEnv("PASTES_DOMAIN", "pastes.sh")
20+	port := utils.GetEnv("PASTES_WEB_PORT", "3000")
21+	dbURL := utils.GetEnv("DATABASE_URL", "")
22+	protocol := utils.GetEnv("PASTES_PROTOCOL", "https")
23+	storageDir := utils.GetEnv("IMGS_STORAGE_DIR", ".storage")
24+	minioURL := utils.GetEnv("MINIO_URL", "")
25+	minioUser := utils.GetEnv("MINIO_ROOT_USER", "")
26+	minioPass := utils.GetEnv("MINIO_ROOT_PASSWORD", "")
27 
28 	return &shared.ConfigSite{
29 		Debug:      debug == "1",
M pastes/scp_hooks.go
+3, -2
 1@@ -11,6 +11,7 @@ import (
 2 	"github.com/picosh/pico/db"
 3 	"github.com/picosh/pico/filehandlers"
 4 	"github.com/picosh/pico/shared"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 var DEFAULT_EXPIRES_AT = 90
 9@@ -21,7 +22,7 @@ type FileHooks struct {
10 }
11 
12 func (p *FileHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaData) (bool, error) {
13-	if !shared.IsTextFile(string(data.Text)) {
14+	if !utils.IsTextFile(string(data.Text)) {
15 		err := fmt.Errorf(
16 			"WARNING: (%s) invalid file must be plain text (utf-8), skipping",
17 			data.Filename,
18@@ -33,7 +34,7 @@ func (p *FileHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaData)
19 }
20 
21 func (p *FileHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData) error {
22-	data.Title = shared.ToUpper(data.Slug)
23+	data.Title = utils.ToUpper(data.Slug)
24 	// we want the slug to be the filename for pastes
25 	data.Slug = data.Filename
26 
M pastes/ssh.go
+8, -8
 1@@ -14,16 +14,16 @@ import (
 2 	"github.com/picosh/pico/db/postgres"
 3 	"github.com/picosh/pico/filehandlers"
 4 	"github.com/picosh/pico/filehandlers/util"
 5-	"github.com/picosh/pico/shared"
 6 	"github.com/picosh/pico/shared/storage"
 7 	wsh "github.com/picosh/pico/wish"
 8+	"github.com/picosh/send/auth"
 9 	"github.com/picosh/send/list"
10 	"github.com/picosh/send/pipe"
11+	wishrsync "github.com/picosh/send/protocols/rsync"
12+	"github.com/picosh/send/protocols/scp"
13+	"github.com/picosh/send/protocols/sftp"
14 	"github.com/picosh/send/proxy"
15-	"github.com/picosh/send/send/auth"
16-	wishrsync "github.com/picosh/send/send/rsync"
17-	"github.com/picosh/send/send/scp"
18-	"github.com/picosh/send/send/sftp"
19+	"github.com/picosh/utils"
20 )
21 
22 func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
23@@ -52,9 +52,9 @@ func withProxy(handler *filehandlers.FileHandlerRouter, otherMiddleware ...wish.
24 }
25 
26 func StartSshServer() {
27-	host := shared.GetEnv("PASTES_HOST", "0.0.0.0")
28-	port := shared.GetEnv("PASTES_SSH_PORT", "2222")
29-	promPort := shared.GetEnv("PASTES_PROM_PORT", "9222")
30+	host := utils.GetEnv("PASTES_HOST", "0.0.0.0")
31+	port := utils.GetEnv("PASTES_SSH_PORT", "2222")
32+	promPort := utils.GetEnv("PASTES_PROM_PORT", "9222")
33 	cfg := NewConfigSite()
34 	logger := cfg.Logger
35 	dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
M pgs/calc_route.go
+1, -1
1@@ -10,7 +10,7 @@ import (
2 
3 	"github.com/picosh/pico/shared"
4 	"github.com/picosh/pico/shared/storage"
5-	"github.com/picosh/send/send/utils"
6+	"github.com/picosh/send/utils"
7 )
8 
9 type HttpReply struct {
M pgs/cli.go
+6, -5
 1@@ -15,6 +15,7 @@ import (
 2 	"github.com/picosh/pico/shared"
 3 	"github.com/picosh/pico/shared/storage"
 4 	"github.com/picosh/pico/tui/common"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 func projectTable(styles common.Styles, projects []*db.Project, width int) *table.Table {
 9@@ -117,7 +118,7 @@ func getHelpText(styles common.Styles, userName string, width int) string {
10 
11 type Cmd struct {
12 	User    *db.User
13-	Session shared.CmdSession
14+	Session utils.CmdSession
15 	Log     *slog.Logger
16 	Store   storage.StorageServe
17 	Dbpool  db.DB
18@@ -209,7 +210,7 @@ func (c *Cmd) statsByProject(projectName string) error {
19 		FkID:     project.ID,
20 		By:       "project_id",
21 		Interval: "day",
22-		Origin:   shared.StartOfMonth(),
23+		Origin:   utils.StartOfMonth(),
24 	}
25 
26 	summary, err := c.Dbpool.VisitSummary(opts)
27@@ -237,7 +238,7 @@ func (c *Cmd) statsSites() error {
28 		FkID:     c.User.ID,
29 		By:       "user_id",
30 		Interval: "day",
31-		Origin:   shared.StartOfMonth(),
32+		Origin:   utils.StartOfMonth(),
33 	}
34 
35 	summary, err := c.Dbpool.VisitSummary(opts)
36@@ -325,8 +326,8 @@ func (c *Cmd) stats(cfgMaxSize uint64) error {
37 
38 	headers := []string{"Used (GB)", "Quota (GB)", "Used (%)", "Projects (#)"}
39 	data := []string{
40-		fmt.Sprintf("%.4f", shared.BytesToGB(int(totalFileSize))),
41-		fmt.Sprintf("%.4f", shared.BytesToGB(int(storageMax))),
42+		fmt.Sprintf("%.4f", utils.BytesToGB(int(totalFileSize))),
43+		fmt.Sprintf("%.4f", utils.BytesToGB(int(storageMax))),
44 		fmt.Sprintf("%.4f", (float32(totalFileSize)/float32(storageMax))*100),
45 		fmt.Sprintf("%d", len(projects)),
46 	}
M pgs/config.go
+12, -11
 1@@ -2,21 +2,22 @@ package pgs
 2 
 3 import (
 4 	"github.com/picosh/pico/shared"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8-var maxSize = uint64(25 * shared.MB)
 9-var maxAssetSize = int64(10 * shared.MB)
10+var maxSize = uint64(25 * utils.MB)
11+var maxAssetSize = int64(10 * utils.MB)
12 
13 func NewConfigSite() *shared.ConfigSite {
14-	domain := shared.GetEnv("PGS_DOMAIN", "pgs.sh")
15-	port := shared.GetEnv("PGS_WEB_PORT", "3000")
16-	protocol := shared.GetEnv("PGS_PROTOCOL", "https")
17-	storageDir := shared.GetEnv("PGS_STORAGE_DIR", ".storage")
18-	minioURL := shared.GetEnv("MINIO_URL", "")
19-	minioUser := shared.GetEnv("MINIO_ROOT_USER", "")
20-	minioPass := shared.GetEnv("MINIO_ROOT_PASSWORD", "")
21-	dbURL := shared.GetEnv("DATABASE_URL", "")
22-	secret := shared.GetEnv("PICO_SECRET", "")
23+	domain := utils.GetEnv("PGS_DOMAIN", "pgs.sh")
24+	port := utils.GetEnv("PGS_WEB_PORT", "3000")
25+	protocol := utils.GetEnv("PGS_PROTOCOL", "https")
26+	storageDir := utils.GetEnv("PGS_STORAGE_DIR", ".storage")
27+	minioURL := utils.GetEnv("MINIO_URL", "")
28+	minioUser := utils.GetEnv("MINIO_ROOT_USER", "")
29+	minioPass := utils.GetEnv("MINIO_ROOT_PASSWORD", "")
30+	dbURL := utils.GetEnv("DATABASE_URL", "")
31+	secret := utils.GetEnv("PICO_SECRET", "")
32 	if secret == "" {
33 		panic("must provide PICO_SECRET environment variable")
34 	}
M pgs/ssh.go
+8, -7
 1@@ -18,14 +18,15 @@ import (
 2 	"github.com/picosh/pico/shared"
 3 	"github.com/picosh/pico/shared/storage"
 4 	wsh "github.com/picosh/pico/wish"
 5+	"github.com/picosh/send/auth"
 6 	"github.com/picosh/send/list"
 7 	"github.com/picosh/send/pipe"
 8+	wishrsync "github.com/picosh/send/protocols/rsync"
 9+	"github.com/picosh/send/protocols/scp"
10+	"github.com/picosh/send/protocols/sftp"
11 	"github.com/picosh/send/proxy"
12-	"github.com/picosh/send/send/auth"
13-	wishrsync "github.com/picosh/send/send/rsync"
14-	"github.com/picosh/send/send/scp"
15-	"github.com/picosh/send/send/sftp"
16 	"github.com/picosh/tunkit"
17+	"github.com/picosh/utils"
18 )
19 
20 func createRouter(cfg *shared.ConfigSite, handler *uploadassets.UploadAssetHandler) proxy.Router {
21@@ -55,9 +56,9 @@ func withProxy(cfg *shared.ConfigSite, handler *uploadassets.UploadAssetHandler,
22 }
23 
24 func StartSshServer() {
25-	host := shared.GetEnv("PGS_HOST", "0.0.0.0")
26-	port := shared.GetEnv("PGS_SSH_PORT", "2222")
27-	promPort := shared.GetEnv("PGS_PROM_PORT", "9222")
28+	host := utils.GetEnv("PGS_HOST", "0.0.0.0")
29+	port := utils.GetEnv("PGS_SSH_PORT", "2222")
30+	promPort := utils.GetEnv("PGS_PROM_PORT", "9222")
31 	cfg := NewConfigSite()
32 	logger := cfg.Logger
33 	dbpool := postgres.NewDB(cfg.DbURL, cfg.Logger)
M pgs/tunnel.go
+4, -5
 1@@ -10,6 +10,7 @@ import (
 2 	"github.com/picosh/pico/db"
 3 	"github.com/picosh/pico/shared"
 4 	"github.com/picosh/pico/ui"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 func allowPerm(proj *db.Project) bool {
 9@@ -42,11 +43,9 @@ func createHttpHandler(apiConfig *shared.ApiConfig) CtxHttpBridge {
10 			log.Error(err.Error(), "subdomain", subdomain)
11 			return http.HandlerFunc(shared.UnauthorizedHandler)
12 		}
13-		pubkeyStr, err := shared.KeyForKeyText(pubkey)
14-		if err != nil {
15-			log.Error(err.Error())
16-			return http.HandlerFunc(shared.UnauthorizedHandler)
17-		}
18+
19+		pubkeyStr := utils.KeyForKeyText(pubkey)
20+
21 		log = log.With(
22 			"pubkey", pubkeyStr,
23 		)
M pgs/wish.go
+6, -6
 1@@ -11,18 +11,18 @@ import (
 2 	bm "github.com/charmbracelet/wish/bubbletea"
 3 	"github.com/picosh/pico/db"
 4 	uploadassets "github.com/picosh/pico/filehandlers/assets"
 5-	"github.com/picosh/pico/shared"
 6 	"github.com/picosh/pico/tui/common"
 7-	"github.com/picosh/send/send/utils"
 8+	sendutils "github.com/picosh/send/utils"
 9+	"github.com/picosh/utils"
10 )
11 
12 func getUser(s ssh.Session, dbpool db.DB) (*db.User, error) {
13-	var err error
14-	key, err := shared.KeyText(s)
15-	if err != nil {
16+	if s.PublicKey() == nil {
17 		return nil, fmt.Errorf("key not found")
18 	}
19 
20+	key := utils.KeyForKeyText(s.PublicKey())
21+
22 	user, err := dbpool.FindUserForKey(s.User(), key)
23 	if err != nil {
24 		return nil, err
25@@ -88,7 +88,7 @@ func WishMiddleware(handler *uploadassets.UploadAssetHandler) wish.Middleware {
26 
27 			user, err := getUser(sesh, dbpool)
28 			if err != nil {
29-				utils.ErrorHandler(sesh, err)
30+				sendutils.ErrorHandler(sesh, err)
31 				return
32 			}
33 
M pico/cli.go
+16, -6
 1@@ -15,15 +15,18 @@ import (
 2 	"github.com/picosh/pico/tui/common"
 3 	"github.com/picosh/pico/tui/notifications"
 4 	"github.com/picosh/pico/tui/plus"
 5+	"github.com/picosh/utils"
 6+
 7+	pipeLogger "github.com/picosh/pubsub/log"
 8 )
 9 
10 func getUser(s ssh.Session, dbpool db.DB) (*db.User, error) {
11-	var err error
12-	key, err := shared.KeyText(s)
13-	if err != nil {
14+	if s.PublicKey() == nil {
15 		return nil, fmt.Errorf("key not found")
16 	}
17 
18+	key := utils.KeyForKeyText(s.PublicKey())
19+
20 	user, err := dbpool.FindUserForKey(s.User(), key)
21 	if err != nil {
22 		return nil, err
23@@ -39,7 +42,7 @@ func getUser(s ssh.Session, dbpool db.DB) (*db.User, error) {
24 type Cmd struct {
25 	User       *db.User
26 	SshSession ssh.Session
27-	Session    shared.CmdSession
28+	Session    utils.CmdSession
29 	Log        *slog.Logger
30 	Dbpool     db.DB
31 	Write      bool
32@@ -67,7 +70,14 @@ func (c *Cmd) notifications() error {
33 }
34 
35 func (c *Cmd) logs(ctx context.Context) error {
36-	stdoutPipe, err := shared.ConnectToLogs(ctx)
37+	stdoutPipe, err := pipeLogger.ConnectToLogs(ctx, &pipeLogger.PubSubConnectionInfo{
38+		RemoteHost:     utils.GetEnv("PICO_PIPE_ENDPOINT", "pipe.pico.sh:22"),
39+		KeyLocation:    utils.GetEnv("PICO_PIPE_KEY", "ssh_data/term_info_ed25519"),
40+		KeyPassphrase:  utils.GetEnv("PICO_PIPE_PASSPHRASE", ""),
41+		RemoteHostname: utils.GetEnv("PICO_PIPE_REMOTE_HOST", "pipe.pico.sh"),
42+		RemoteUser:     utils.GetEnv("PICO_PIPE_USER", "pico"),
43+	})
44+
45 	if err != nil {
46 		return err
47 	}
48@@ -83,7 +93,7 @@ func (c *Cmd) logs(ctx context.Context) error {
49 			continue
50 		}
51 
52-		user := shared.AnyToStr(parsedData, "user")
53+		user := utils.AnyToStr(parsedData, "user")
54 		if user == c.User.Name {
55 			wish.Println(c.SshSession, line)
56 		}
M pico/config.go
+2, -1
 1@@ -2,10 +2,11 @@ package pico
 2 
 3 import (
 4 	"github.com/picosh/pico/shared"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 func NewConfigSite() *shared.ConfigSite {
 9-	dbURL := shared.GetEnv("DATABASE_URL", "")
10+	dbURL := utils.GetEnv("DATABASE_URL", "")
11 
12 	return &shared.ConfigSite{
13 		DbURL:  dbURL,
M pico/file_handler.go
+12, -17
  1@@ -16,7 +16,8 @@ import (
  2 	"github.com/picosh/pico/db"
  3 	"github.com/picosh/pico/filehandlers/util"
  4 	"github.com/picosh/pico/shared"
  5-	"github.com/picosh/send/send/utils"
  6+	sendutils "github.com/picosh/send/utils"
  7+	"github.com/picosh/utils"
  8 )
  9 
 10 type UploadHandler struct {
 11@@ -31,7 +32,7 @@ func NewUploadHandler(dbpool db.DB, cfg *shared.ConfigSite) *UploadHandler {
 12 	}
 13 }
 14 
 15-func (h *UploadHandler) getAuthorizedKeyFile(user *db.User) (*utils.VirtualFile, string, error) {
 16+func (h *UploadHandler) getAuthorizedKeyFile(user *db.User) (*sendutils.VirtualFile, string, error) {
 17 	keys, err := h.DBPool.FindKeysForUser(user)
 18 	text := ""
 19 	var modTime time.Time
 20@@ -42,7 +43,7 @@ func (h *UploadHandler) getAuthorizedKeyFile(user *db.User) (*utils.VirtualFile,
 21 	if err != nil {
 22 		return nil, "", err
 23 	}
 24-	fileInfo := &utils.VirtualFile{
 25+	fileInfo := &sendutils.VirtualFile{
 26 		FName:    "authorized_keys",
 27 		FIsDir:   false,
 28 		FSize:    int64(len(text)),
 29@@ -51,11 +52,11 @@ func (h *UploadHandler) getAuthorizedKeyFile(user *db.User) (*utils.VirtualFile,
 30 	return fileInfo, text, nil
 31 }
 32 
 33-func (h *UploadHandler) Delete(s ssh.Session, entry *utils.FileEntry) error {
 34+func (h *UploadHandler) Delete(s ssh.Session, entry *sendutils.FileEntry) error {
 35 	return errors.New("unsupported")
 36 }
 37 
 38-func (h *UploadHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileInfo, utils.ReaderAtCloser, error) {
 39+func (h *UploadHandler) Read(s ssh.Session, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReaderAtCloser, error) {
 40 	user, err := util.GetUser(s.Context())
 41 	if err != nil {
 42 		return nil, nil, err
 43@@ -71,7 +72,7 @@ func (h *UploadHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileInfo
 44 		if err != nil {
 45 			return nil, nil, err
 46 		}
 47-		reader := utils.NopReaderAtCloser(strings.NewReader(text))
 48+		reader := sendutils.NopReaderAtCloser(strings.NewReader(text))
 49 		return fileInfo, reader, nil
 50 	}
 51 
 52@@ -92,7 +93,7 @@ func (h *UploadHandler) List(s ssh.Session, fpath string, isDir bool, recursive
 53 			name = "/"
 54 		}
 55 
 56-		fileList = append(fileList, &utils.VirtualFile{
 57+		fileList = append(fileList, &sendutils.VirtualFile{
 58 			FName:  name,
 59 			FIsDir: true,
 60 		})
 61@@ -121,7 +122,7 @@ func (h *UploadHandler) GetLogger() *slog.Logger {
 62 
 63 func (h *UploadHandler) Validate(s ssh.Session) error {
 64 	var err error
 65-	key, err := utils.KeyText(s)
 66+	key, err := sendutils.KeyText(s)
 67 	if err != nil {
 68 		return fmt.Errorf("key not found")
 69 	}
 70@@ -231,10 +232,7 @@ func (h *UploadHandler) ProcessAuthorizedKeys(text []byte, logger *slog.Logger,
 71 	diff := authorizedKeysDiff(s.PublicKey(), curKeys, nextKeys)
 72 
 73 	for _, pk := range diff.Add {
 74-		key, err := shared.KeyForKeyText(pk.Pk)
 75-		if err != nil {
 76-			continue
 77-		}
 78+		key := utils.KeyForKeyText(pk.Pk)
 79 
 80 		wish.Errorf(s, "adding pubkey (%s)\n", key)
 81 		logger.Info("adding pubkey", "pubkey", key)
 82@@ -247,10 +245,7 @@ func (h *UploadHandler) ProcessAuthorizedKeys(text []byte, logger *slog.Logger,
 83 	}
 84 
 85 	for _, pk := range diff.Update {
 86-		key, err := shared.KeyForKeyText(pk.Pk)
 87-		if err != nil {
 88-			continue
 89-		}
 90+		key := utils.KeyForKeyText(pk.Pk)
 91 
 92 		wish.Errorf(s, "updating pubkey with comment: %s (%s)\n", pk.Comment, key)
 93 		logger.Info(
 94@@ -280,7 +275,7 @@ func (h *UploadHandler) ProcessAuthorizedKeys(text []byte, logger *slog.Logger,
 95 	return nil
 96 }
 97 
 98-func (h *UploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string, error) {
 99+func (h *UploadHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (string, error) {
100 	logger := h.Cfg.Logger
101 	user, err := util.GetUser(s.Context())
102 	if err != nil {
M pico/ssh.go
+8, -7
 1@@ -17,13 +17,14 @@ import (
 2 	"github.com/picosh/pico/shared"
 3 	"github.com/picosh/pico/tui"
 4 	wsh "github.com/picosh/pico/wish"
 5+	"github.com/picosh/send/auth"
 6 	"github.com/picosh/send/list"
 7 	"github.com/picosh/send/pipe"
 8+	wishrsync "github.com/picosh/send/protocols/rsync"
 9+	"github.com/picosh/send/protocols/scp"
10+	"github.com/picosh/send/protocols/sftp"
11 	"github.com/picosh/send/proxy"
12-	"github.com/picosh/send/send/auth"
13-	wishrsync "github.com/picosh/send/send/rsync"
14-	"github.com/picosh/send/send/scp"
15-	"github.com/picosh/send/send/sftp"
16+	"github.com/picosh/utils"
17 )
18 
19 func authHandler(ctx ssh.Context, key ssh.PublicKey) bool {
20@@ -58,9 +59,9 @@ func withProxy(cfg *shared.ConfigSite, handler *UploadHandler, cliHandler *CliHa
21 }
22 
23 func StartSshServer() {
24-	host := shared.GetEnv("PICO_HOST", "0.0.0.0")
25-	port := shared.GetEnv("PICO_SSH_PORT", "2222")
26-	promPort := shared.GetEnv("PICO_PROM_PORT", "9222")
27+	host := utils.GetEnv("PICO_HOST", "0.0.0.0")
28+	port := utils.GetEnv("PICO_SSH_PORT", "2222")
29+	promPort := utils.GetEnv("PICO_PROM_PORT", "9222")
30 	cfg := NewConfigSite()
31 	logger := cfg.Logger
32 	dbpool := postgres.NewDB(cfg.DbURL, cfg.Logger)
M prose/api.go
+7, -6
 1@@ -20,6 +20,7 @@ import (
 2 	"github.com/picosh/pico/imgs"
 3 	"github.com/picosh/pico/shared"
 4 	"github.com/picosh/pico/shared/storage"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 type PageData struct {
 9@@ -260,10 +261,10 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
10 		p := PostItemData{
11 			URL:            template.URL(cfg.FullPostURL(curl, post.Username, post.Slug)),
12 			BlogURL:        template.URL(cfg.FullBlogURL(curl, post.Username)),
13-			Title:          shared.FilenameToTitle(post.Filename, post.Title),
14+			Title:          utils.FilenameToTitle(post.Filename, post.Title),
15 			PublishAt:      post.PublishAt.Format(time.DateOnly),
16 			PublishAtISO:   post.PublishAt.Format(time.RFC3339),
17-			UpdatedTimeAgo: shared.TimeAgo(post.UpdatedAt),
18+			UpdatedTimeAgo: utils.TimeAgo(post.UpdatedAt),
19 			UpdatedAtISO:   post.UpdatedAt.Format(time.RFC3339),
20 		}
21 		postCollection = append(postCollection, p)
22@@ -446,7 +447,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
23 			URL:          template.URL(cfg.FullPostURL(curl, post.Username, post.Slug)),
24 			BlogURL:      template.URL(cfg.FullBlogURL(curl, username)),
25 			Description:  post.Description,
26-			Title:        shared.FilenameToTitle(post.Filename, post.Title),
27+			Title:        utils.FilenameToTitle(post.Filename, post.Title),
28 			Slug:         post.Slug,
29 			PublishAt:    post.PublishAt.Format(time.DateOnly),
30 			PublishAtISO: post.PublishAt.Format(time.RFC3339),
31@@ -595,12 +596,12 @@ func readHandler(w http.ResponseWriter, r *http.Request) {
32 		item := PostItemData{
33 			URL:            template.URL(cfg.FullPostURL(curl, post.Username, post.Slug)),
34 			BlogURL:        template.URL(cfg.FullBlogURL(curl, post.Username)),
35-			Title:          shared.FilenameToTitle(post.Filename, post.Title),
36+			Title:          utils.FilenameToTitle(post.Filename, post.Title),
37 			Description:    post.Description,
38 			Username:       post.Username,
39 			PublishAt:      post.PublishAt.Format(time.DateOnly),
40 			PublishAtISO:   post.PublishAt.Format(time.RFC3339),
41-			UpdatedTimeAgo: shared.TimeAgo(post.UpdatedAt),
42+			UpdatedTimeAgo: utils.TimeAgo(post.UpdatedAt),
43 			UpdatedAtISO:   post.UpdatedAt.Format(time.RFC3339),
44 		}
45 		data.Posts = append(data.Posts, item)
46@@ -725,7 +726,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
47 
48 		item := &feeds.Item{
49 			Id:          feedId,
50-			Title:       shared.FilenameToTitle(post.Filename, post.Title),
51+			Title:       utils.FilenameToTitle(post.Filename, post.Title),
52 			Link:        &feeds.Link{Href: realUrl},
53 			Content:     tpl.String(),
54 			Updated:     *post.UpdatedAt,
M prose/cli.go
+7, -7
 1@@ -10,17 +10,17 @@ import (
 2 	"github.com/charmbracelet/wish"
 3 	bm "github.com/charmbracelet/wish/bubbletea"
 4 	"github.com/picosh/pico/db"
 5-	"github.com/picosh/pico/shared"
 6 	"github.com/picosh/pico/tui/common"
 7+	"github.com/picosh/utils"
 8 )
 9 
10 func getUser(s ssh.Session, dbpool db.DB) (*db.User, error) {
11-	var err error
12-	key, err := shared.KeyText(s)
13-	if err != nil {
14+	if s.PublicKey() == nil {
15 		return nil, fmt.Errorf("key not found")
16 	}
17 
18+	key := utils.KeyForKeyText(s.PublicKey())
19+
20 	user, err := dbpool.FindUserForKey(s.User(), key)
21 	if err != nil {
22 		return nil, err
23@@ -35,7 +35,7 @@ func getUser(s ssh.Session, dbpool db.DB) (*db.User, error) {
24 
25 type Cmd struct {
26 	User    *db.User
27-	Session shared.CmdSession
28+	Session utils.CmdSession
29 	Log     *slog.Logger
30 	Dbpool  db.DB
31 	Styles  common.Styles
32@@ -62,7 +62,7 @@ func (c *Cmd) statsByPost(postSlug string) error {
33 		FkID:     post.ID,
34 		By:       "post_id",
35 		Interval: "day",
36-		Origin:   shared.StartOfMonth(),
37+		Origin:   utils.StartOfMonth(),
38 	}
39 
40 	summary, err := c.Dbpool.VisitSummary(opts)
41@@ -90,7 +90,7 @@ func (c *Cmd) stats() error {
42 		FkID:     c.User.ID,
43 		By:       "user_id",
44 		Interval: "day",
45-		Origin:   shared.StartOfMonth(),
46+		Origin:   utils.StartOfMonth(),
47 		Where:    "AND (post_id IS NOT NULL OR (post_id IS NULL AND project_id IS NULL))",
48 	}
49 
M prose/config.go
+13, -12
 1@@ -2,21 +2,22 @@ package prose
 2 
 3 import (
 4 	"github.com/picosh/pico/shared"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 func NewConfigSite() *shared.ConfigSite {
 9-	debug := shared.GetEnv("PROSE_DEBUG", "0")
10-	domain := shared.GetEnv("PROSE_DOMAIN", "prose.sh")
11-	port := shared.GetEnv("PROSE_WEB_PORT", "3000")
12-	protocol := shared.GetEnv("PROSE_PROTOCOL", "https")
13-	storageDir := shared.GetEnv("IMGS_STORAGE_DIR", ".storage")
14-	minioURL := shared.GetEnv("MINIO_URL", "")
15-	minioUser := shared.GetEnv("MINIO_ROOT_USER", "")
16-	minioPass := shared.GetEnv("MINIO_ROOT_PASSWORD", "")
17-	dbURL := shared.GetEnv("DATABASE_URL", "")
18-	maxSize := uint64(500 * shared.MB)
19-	maxImgSize := int64(10 * shared.MB)
20-	secret := shared.GetEnv("PICO_SECRET", "")
21+	debug := utils.GetEnv("PROSE_DEBUG", "0")
22+	domain := utils.GetEnv("PROSE_DOMAIN", "prose.sh")
23+	port := utils.GetEnv("PROSE_WEB_PORT", "3000")
24+	protocol := utils.GetEnv("PROSE_PROTOCOL", "https")
25+	storageDir := utils.GetEnv("IMGS_STORAGE_DIR", ".storage")
26+	minioURL := utils.GetEnv("MINIO_URL", "")
27+	minioUser := utils.GetEnv("MINIO_ROOT_USER", "")
28+	minioPass := utils.GetEnv("MINIO_ROOT_PASSWORD", "")
29+	dbURL := utils.GetEnv("DATABASE_URL", "")
30+	maxSize := uint64(500 * utils.MB)
31+	maxImgSize := int64(10 * utils.MB)
32+	secret := utils.GetEnv("PICO_SECRET", "")
33 	if secret == "" {
34 		panic("must provide PICO_SECRET environment variable")
35 	}
M prose/scp_hooks.go
+4, -3
 1@@ -10,6 +10,7 @@ import (
 2 	"github.com/picosh/pico/db"
 3 	"github.com/picosh/pico/filehandlers"
 4 	"github.com/picosh/pico/shared"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 type MarkdownHooks struct {
 9@@ -18,7 +19,7 @@ type MarkdownHooks struct {
10 }
11 
12 func (p *MarkdownHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaData) (bool, error) {
13-	if !shared.IsTextFile(data.Text) {
14+	if !utils.IsTextFile(data.Text) {
15 		err := fmt.Errorf(
16 			"WARNING: (%s) invalid file must be plain text (utf-8), skipping",
17 			data.Filename,
18@@ -33,7 +34,7 @@ func (p *MarkdownHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaD
19 		return true, nil
20 	}
21 
22-	if !shared.IsExtAllowed(data.Filename, p.Cfg.AllowedExt) {
23+	if !utils.IsExtAllowed(data.Filename, p.Cfg.AllowedExt) {
24 		extStr := strings.Join(p.Cfg.AllowedExt, ",")
25 		err := fmt.Errorf(
26 			"WARNING: (%s) invalid file, format must be (%s), skipping",
27@@ -53,7 +54,7 @@ func (p *MarkdownHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData)
28 	}
29 
30 	if parsedText.Title == "" {
31-		data.Title = shared.ToUpper(data.Slug)
32+		data.Title = utils.ToUpper(data.Slug)
33 	} else {
34 		data.Title = parsedText.Title
35 	}
M prose/ssh.go
+8, -8
 1@@ -15,16 +15,16 @@ import (
 2 	"github.com/picosh/pico/filehandlers"
 3 	uploadimgs "github.com/picosh/pico/filehandlers/imgs"
 4 	"github.com/picosh/pico/filehandlers/util"
 5-	"github.com/picosh/pico/shared"
 6 	"github.com/picosh/pico/shared/storage"
 7 	wsh "github.com/picosh/pico/wish"
 8+	"github.com/picosh/send/auth"
 9 	"github.com/picosh/send/list"
10 	"github.com/picosh/send/pipe"
11+	wishrsync "github.com/picosh/send/protocols/rsync"
12+	"github.com/picosh/send/protocols/scp"
13+	"github.com/picosh/send/protocols/sftp"
14 	"github.com/picosh/send/proxy"
15-	"github.com/picosh/send/send/auth"
16-	wishrsync "github.com/picosh/send/send/rsync"
17-	"github.com/picosh/send/send/scp"
18-	"github.com/picosh/send/send/sftp"
19+	"github.com/picosh/utils"
20 )
21 
22 func createRouter(handler *filehandlers.FileHandlerRouter, cliHandler *CliHandler) proxy.Router {
23@@ -54,9 +54,9 @@ func withProxy(handler *filehandlers.FileHandlerRouter, cliHandler *CliHandler,
24 }
25 
26 func StartSshServer() {
27-	host := shared.GetEnv("PROSE_HOST", "0.0.0.0")
28-	port := shared.GetEnv("PROSE_SSH_PORT", "2222")
29-	promPort := shared.GetEnv("PROSE_PROM_PORT", "9222")
30+	host := utils.GetEnv("PROSE_HOST", "0.0.0.0")
31+	port := utils.GetEnv("PROSE_SSH_PORT", "2222")
32+	promPort := utils.GetEnv("PROSE_PROM_PORT", "9222")
33 	cfg := NewConfigSite()
34 	logger := cfg.Logger
35 	dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
M pubsub/cli.go
+2, -2
 1@@ -17,7 +17,7 @@ import (
 2 	"github.com/picosh/pico/db"
 3 	"github.com/picosh/pico/shared"
 4 	psub "github.com/picosh/pubsub"
 5-	"github.com/picosh/send/send/utils"
 6+	"github.com/picosh/send/utils"
 7 )
 8 
 9 func flagSet(cmdName string, sesh ssh.Session) *flag.FlagSet {
10@@ -48,7 +48,7 @@ func NewTabWriter(out io.Writer) *tabwriter.Writer {
11 
12 func getUser(s ssh.Session, dbpool db.DB) (*db.User, error) {
13 	var err error
14-	key, err := shared.KeyText(s)
15+	key, err := utils.KeyText(s)
16 	if err != nil {
17 		return nil, fmt.Errorf("key not found")
18 	}
M pubsub/config.go
+5, -4
 1@@ -2,13 +2,14 @@ package pubsub
 2 
 3 import (
 4 	"github.com/picosh/pico/shared"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 func NewConfigSite() *shared.ConfigSite {
 9-	domain := shared.GetEnv("PUBSUB_DOMAIN", "send.pico.sh")
10-	port := shared.GetEnv("PUBSUB_WEB_PORT", "3000")
11-	dbURL := shared.GetEnv("DATABASE_URL", "")
12-	protocol := shared.GetEnv("PUBSUB_PROTOCOL", "https")
13+	domain := utils.GetEnv("PUBSUB_DOMAIN", "pipe.pico.sh")
14+	port := utils.GetEnv("PUBSUB_WEB_PORT", "3000")
15+	dbURL := utils.GetEnv("DATABASE_URL", "")
16+	protocol := utils.GetEnv("PUBSUB_PROTOCOL", "https")
17 
18 	return &shared.ConfigSite{
19 		Domain:   domain,
M pubsub/ssh.go
+5, -5
 1@@ -12,16 +12,16 @@ import (
 2 	"github.com/charmbracelet/wish"
 3 	"github.com/picosh/pico/db/postgres"
 4 	"github.com/picosh/pico/filehandlers/util"
 5-	"github.com/picosh/pico/shared"
 6 	wsh "github.com/picosh/pico/wish"
 7 	psub "github.com/picosh/pubsub"
 8+	"github.com/picosh/utils"
 9 )
10 
11 func StartSshServer() {
12-	host := shared.GetEnv("PUBSUB_HOST", "0.0.0.0")
13-	port := shared.GetEnv("PUBSUB_SSH_PORT", "2222")
14-	portOverride := shared.GetEnv("PUBSUB_SSH_PORT_OVERRIDE", port)
15-	promPort := shared.GetEnv("PUBSUB_PROM_PORT", "9222")
16+	host := utils.GetEnv("PUBSUB_HOST", "0.0.0.0")
17+	port := utils.GetEnv("PUBSUB_SSH_PORT", "2222")
18+	portOverride := utils.GetEnv("PUBSUB_SSH_PORT_OVERRIDE", port)
19+	promPort := utils.GetEnv("PUBSUB_PROM_PORT", "9222")
20 	cfg := NewConfigSite()
21 	logger := cfg.Logger
22 	dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
M shared/api.go
+2, -1
 1@@ -10,6 +10,7 @@ import (
 2 
 3 	"github.com/charmbracelet/ssh"
 4 	"github.com/picosh/pico/db"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 func CorsHeaders(headers http.Header) {
 9@@ -43,7 +44,7 @@ type UserApi struct {
10 func NewUserApi(user *db.User, pubkey ssh.PublicKey) *UserApi {
11 	return &UserApi{
12 		User:        user,
13-		Fingerprint: KeyForSha256(pubkey),
14+		Fingerprint: utils.KeyForSha256(pubkey),
15 	}
16 }
17 
M shared/bucket.go
+1, -1
1@@ -6,7 +6,7 @@ import (
2 	"path/filepath"
3 	"strings"
4 
5-	"github.com/picosh/send/send/utils"
6+	"github.com/picosh/send/utils"
7 )
8 
9 func GetImgsBucketName(userID string) string {
D shared/cmd.go
+0, -37
 1@@ -1,37 +0,0 @@
 2-package shared
 3-
 4-import (
 5-	"fmt"
 6-	"io"
 7-	"log/slog"
 8-	"os"
 9-)
10-
11-type CmdSessionLogger struct {
12-	Log *slog.Logger
13-}
14-
15-func (c *CmdSessionLogger) Write(out []byte) (int, error) {
16-	c.Log.Info(string(out))
17-	return 0, nil
18-}
19-
20-func (c *CmdSessionLogger) Exit(code int) error {
21-	os.Exit(code)
22-	return fmt.Errorf("panic %d", code)
23-}
24-
25-func (c *CmdSessionLogger) Close() error {
26-	return fmt.Errorf("closing")
27-}
28-
29-func (c *CmdSessionLogger) Stderr() io.ReadWriter {
30-	return nil
31-}
32-
33-type CmdSession interface {
34-	Write([]byte) (int, error)
35-	Exit(code int) error
36-	Close() error
37-	Stderr() io.ReadWriter
38-}
M shared/config.go
+12, -2
 1@@ -11,6 +11,9 @@ import (
 2 	"strings"
 3 
 4 	"github.com/picosh/pico/db"
 5+	"github.com/picosh/utils"
 6+
 7+	pipeLogger "github.com/picosh/pubsub/log"
 8 )
 9 
10 var DefaultEmail = "hello@pico.sh"
11@@ -274,8 +277,15 @@ func CreateLogger(space string) *slog.Logger {
12 
13 	newLogger := log
14 
15-	if strings.ToLower(GetEnv("PICO_SENDLOG_ENABLED", "true")) == "true" {
16-		newLog, err := SendLogRegister(log, 100)
17+	if strings.ToLower(utils.GetEnv("PICO_PIPE_ENABLED", "true")) == "true" {
18+		newLog, err := pipeLogger.SendLogRegister(log, &pipeLogger.PubSubConnectionInfo{
19+			RemoteHost:     utils.GetEnv("PICO_PIPE_ENDPOINT", "pipe.pico.sh:22"),
20+			KeyLocation:    utils.GetEnv("PICO_PIPE_KEY", "ssh_data/term_info_ed25519"),
21+			KeyPassphrase:  utils.GetEnv("PICO_PIPE_PASSPHRASE", ""),
22+			RemoteHostname: utils.GetEnv("PICO_PIPE_REMOTE_HOST", "pipe.pico.sh"),
23+			RemoteUser:     utils.GetEnv("PICO_PIPE_USER", "pico"),
24+		}, 100)
25+
26 		if err == nil {
27 			newLogger = newLog
28 		} else {
D shared/io.go
+0, -35
 1@@ -1,35 +0,0 @@
 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: %.2fmb", 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-}
D shared/sendlog.go
+0, -386
  1@@ -1,386 +0,0 @@
  2-package shared
  3-
  4-import (
  5-	"context"
  6-	"errors"
  7-	"fmt"
  8-	"io"
  9-	"log/slog"
 10-	"net"
 11-	"os"
 12-	"path/filepath"
 13-	"slices"
 14-	"strings"
 15-	"sync"
 16-	"time"
 17-
 18-	"golang.org/x/crypto/ssh"
 19-)
 20-
 21-type MultiHandler struct {
 22-	Handlers []slog.Handler
 23-	mu       sync.Mutex
 24-}
 25-
 26-func (m *MultiHandler) Enabled(ctx context.Context, l slog.Level) bool {
 27-	m.mu.Lock()
 28-	defer m.mu.Unlock()
 29-
 30-	for _, h := range m.Handlers {
 31-		if h.Enabled(ctx, l) {
 32-			return true
 33-		}
 34-	}
 35-
 36-	return false
 37-}
 38-
 39-func (m *MultiHandler) Handle(ctx context.Context, r slog.Record) error {
 40-	m.mu.Lock()
 41-	defer m.mu.Unlock()
 42-
 43-	var errs []error
 44-	for _, h := range m.Handlers {
 45-		if h.Enabled(ctx, r.Level) {
 46-			errs = append(errs, h.Handle(ctx, r.Clone()))
 47-		}
 48-	}
 49-
 50-	return errors.Join(errs...)
 51-}
 52-
 53-func (m *MultiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
 54-	m.mu.Lock()
 55-	defer m.mu.Unlock()
 56-
 57-	var handlers []slog.Handler
 58-
 59-	for _, h := range m.Handlers {
 60-		handlers = append(handlers, h.WithAttrs(slices.Clone(attrs)))
 61-	}
 62-
 63-	return &MultiHandler{
 64-		Handlers: handlers,
 65-	}
 66-}
 67-
 68-func (m *MultiHandler) WithGroup(name string) slog.Handler {
 69-	if name == "" {
 70-		return m
 71-	}
 72-
 73-	m.mu.Lock()
 74-	defer m.mu.Unlock()
 75-
 76-	var handlers []slog.Handler
 77-
 78-	for _, h := range m.Handlers {
 79-		handlers = append(handlers, h.WithGroup(name))
 80-	}
 81-
 82-	return &MultiHandler{
 83-		Handlers: handlers,
 84-	}
 85-}
 86-
 87-type SendLogWriter struct {
 88-	SSHClient        *ssh.Client
 89-	Session          *ssh.Session
 90-	StdinPipe        io.WriteCloser
 91-	Done             chan struct{}
 92-	Messages         chan []byte
 93-	Timeout          time.Duration
 94-	BufferSize       int
 95-	closeOnce        sync.Once
 96-	closeMessageOnce sync.Once
 97-	startOnce        sync.Once
 98-	connecMu         sync.Mutex
 99-}
100-
101-func (c *SendLogWriter) Close() error {
102-	c.connecMu.Lock()
103-	defer c.connecMu.Unlock()
104-
105-	if c.Done != nil {
106-		c.closeOnce.Do(func() {
107-			close(c.Done)
108-		})
109-	}
110-
111-	if c.Messages != nil {
112-		c.closeMessageOnce.Do(func() {
113-			close(c.Messages)
114-		})
115-	}
116-
117-	var errs []error
118-
119-	if c.StdinPipe != nil {
120-		errs = append(errs, c.StdinPipe.Close())
121-	}
122-
123-	if c.Session != nil {
124-		errs = append(errs, c.Session.Close())
125-	}
126-
127-	if c.SSHClient != nil {
128-		errs = append(errs, c.SSHClient.Close())
129-	}
130-
131-	return errors.Join(errs...)
132-}
133-
134-func (c *SendLogWriter) Open() error {
135-	c.Close()
136-
137-	c.connecMu.Lock()
138-
139-	c.Done = make(chan struct{})
140-	c.Messages = make(chan []byte, c.BufferSize)
141-
142-	sshClient, err := CreateSSHClient(
143-		GetEnv("PICO_SENDLOG_ENDPOINT", "send.pico.sh:22"),
144-		GetEnv("PICO_SENDLOG_KEY", "ssh_data/term_info_ed25519"),
145-		GetEnv("PICO_SENDLOG_PASSPHRASE", ""),
146-		GetEnv("PICO_SENDLOG_REMOTE_HOST", "send.pico.sh"),
147-		GetEnv("PICO_SENDLOG_USER", "pico"),
148-	)
149-	if err != nil {
150-		c.connecMu.Unlock()
151-		return err
152-	}
153-
154-	session, err := sshClient.NewSession()
155-	if err != nil {
156-		c.connecMu.Unlock()
157-		return err
158-	}
159-
160-	stdinPipe, err := session.StdinPipe()
161-	if err != nil {
162-		c.connecMu.Unlock()
163-		return err
164-	}
165-
166-	err = session.Start("pub log-drain -b=false")
167-	if err != nil {
168-		c.connecMu.Unlock()
169-		return err
170-	}
171-
172-	c.SSHClient = sshClient
173-	c.Session = session
174-	c.StdinPipe = stdinPipe
175-
176-	c.closeOnce = sync.Once{}
177-	c.startOnce = sync.Once{}
178-
179-	c.connecMu.Unlock()
180-
181-	c.Start()
182-
183-	return nil
184-}
185-
186-func (c *SendLogWriter) Start() {
187-	c.startOnce.Do(func() {
188-		go func() {
189-			defer c.Reconnect()
190-
191-			for {
192-				select {
193-				case data, ok := <-c.Messages:
194-					_, err := c.StdinPipe.Write(data)
195-					if !ok || err != nil {
196-						slog.Error("received error on write, reopening logger", "error", err)
197-						return
198-					}
199-				case <-c.Done:
200-					return
201-				}
202-			}
203-		}()
204-	})
205-}
206-
207-func (c *SendLogWriter) Write(data []byte) (int, error) {
208-	var (
209-		n   int
210-		err error
211-	)
212-
213-	ok := c.connecMu.TryLock()
214-
215-	if !ok {
216-		return n, fmt.Errorf("unable to acquire lock to write")
217-	}
218-
219-	defer c.connecMu.Unlock()
220-
221-	if c.Messages == nil || c.Done == nil {
222-		return n, fmt.Errorf("logger not viable")
223-	}
224-
225-	select {
226-	case c.Messages <- slices.Clone(data):
227-		n = len(data)
228-	case <-time.After(c.Timeout):
229-		err = fmt.Errorf("unable to send data within timeout")
230-	case <-c.Done:
231-		break
232-	}
233-
234-	return n, err
235-}
236-
237-func (c *SendLogWriter) Reconnect() {
238-	go func() {
239-		for {
240-			err := c.Open()
241-			if err != nil {
242-				slog.Error("unable to open send logger. retrying in 10 seconds", "error", err)
243-			} else {
244-				return
245-			}
246-
247-			<-time.After(10 * time.Second)
248-		}
249-	}()
250-}
251-
252-func CreateSSHClient(remoteHost string, keyLocation string, keyPassphrase string, remoteHostname string, remoteUser string) (*ssh.Client, error) {
253-	if !strings.Contains(remoteHost, ":") {
254-		remoteHost += ":22"
255-	}
256-
257-	rawConn, err := net.Dial("tcp", remoteHost)
258-	if err != nil {
259-		return nil, err
260-	}
261-
262-	keyPath, err := filepath.Abs(keyLocation)
263-	if err != nil {
264-		return nil, err
265-	}
266-
267-	f, err := os.Open(keyPath)
268-	if err != nil {
269-		return nil, err
270-	}
271-	defer f.Close()
272-
273-	data, err := io.ReadAll(f)
274-	if err != nil {
275-		return nil, err
276-	}
277-
278-	var signer ssh.Signer
279-
280-	if keyPassphrase != "" {
281-		signer, err = ssh.ParsePrivateKeyWithPassphrase(data, []byte(keyPassphrase))
282-	} else {
283-		signer, err = ssh.ParsePrivateKey(data)
284-	}
285-
286-	if err != nil {
287-		return nil, err
288-	}
289-
290-	sshConn, chans, reqs, err := ssh.NewClientConn(rawConn, remoteHostname, &ssh.ClientConfig{
291-		Auth:            []ssh.AuthMethod{ssh.PublicKeys(signer)},
292-		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
293-		User:            remoteUser,
294-	})
295-
296-	if err != nil {
297-		return nil, err
298-	}
299-
300-	sshClient := ssh.NewClient(sshConn, chans, reqs)
301-
302-	return sshClient, nil
303-}
304-
305-func SendLogRegister(logger *slog.Logger, buffer int) (*slog.Logger, error) {
306-	if buffer < 0 {
307-		buffer = 0
308-	}
309-
310-	currentHandler := logger.Handler()
311-
312-	logWriter := &SendLogWriter{
313-		Timeout:    10 * time.Millisecond,
314-		BufferSize: buffer,
315-	}
316-
317-	logWriter.Reconnect()
318-
319-	return slog.New(
320-		&MultiHandler{
321-			Handlers: []slog.Handler{
322-				currentHandler,
323-				slog.NewJSONHandler(logWriter, &slog.HandlerOptions{
324-					AddSource: true,
325-					Level:     slog.LevelDebug,
326-				}),
327-			},
328-		},
329-	), nil
330-}
331-
332-var _ io.Writer = (*SendLogWriter)(nil)
333-var _ slog.Handler = (*MultiHandler)(nil)
334-
335-func AnyToStr(mp map[string]any, key string) string {
336-	if value, ok := mp[key]; ok {
337-		if value, ok := value.(string); ok {
338-			return value
339-		}
340-	}
341-	return ""
342-}
343-
344-func AnyToFloat(mp map[string]any, key string) float64 {
345-	if value, ok := mp[key]; ok {
346-		if value, ok := value.(float64); ok {
347-			return value
348-		}
349-	}
350-	return 0
351-}
352-
353-func ConnectToLogs(ctx context.Context) (io.Reader, error) {
354-	sshClient, err := CreateSSHClient(
355-		GetEnv("PICO_SENDLOG_ENDPOINT", "send.pico.sh:22"),
356-		GetEnv("PICO_SENDLOG_KEY", "ssh_data/term_info_ed25519"),
357-		GetEnv("PICO_SENDLOG_PASSPHRASE", ""),
358-		GetEnv("PICO_SENDLOG_REMOTE_HOST", "send.pico.sh"),
359-		GetEnv("PICO_SENDLOG_USER", "pico"),
360-	)
361-	if err != nil {
362-		return nil, err
363-	}
364-
365-	session, err := sshClient.NewSession()
366-	if err != nil {
367-		return nil, err
368-	}
369-
370-	stdoutPipe, err := session.StdoutPipe()
371-	if err != nil {
372-		return nil, err
373-	}
374-
375-	err = session.Start("sub log-drain -k")
376-	if err != nil {
377-		return nil, err
378-	}
379-
380-	go func() {
381-		<-ctx.Done()
382-		session.Close()
383-		sshClient.Close()
384-	}()
385-
386-	return stdoutPipe, nil
387-}
D shared/util.go
+0, -169
  1@@ -1,169 +0,0 @@
  2-package shared
  3-
  4-import (
  5-	"crypto/sha256"
  6-	"encoding/base64"
  7-	"encoding/hex"
  8-	"fmt"
  9-	"math"
 10-	"os"
 11-	pathpkg "path"
 12-	"path/filepath"
 13-	"regexp"
 14-	"strings"
 15-	"time"
 16-	"unicode"
 17-	"unicode/utf8"
 18-
 19-	"slices"
 20-
 21-	"github.com/charmbracelet/ssh"
 22-	gossh "golang.org/x/crypto/ssh"
 23-)
 24-
 25-var fnameRe = regexp.MustCompile(`[-_]+`)
 26-var subdomainRe = regexp.MustCompile(`^[a-z0-9-]+$`)
 27-
 28-var KB = 1000
 29-var MB = KB * 1000
 30-
 31-func IsValidSubdomain(subd string) bool {
 32-	return subdomainRe.MatchString(subd)
 33-}
 34-
 35-func FilenameToTitle(filename string, title string) string {
 36-	if filename != title {
 37-		return title
 38-	}
 39-
 40-	return ToUpper(title)
 41-}
 42-
 43-func ToUpper(str string) string {
 44-	pre := fnameRe.ReplaceAllString(str, " ")
 45-
 46-	r := []rune(pre)
 47-	if len(r) > 0 {
 48-		r[0] = unicode.ToUpper(r[0])
 49-	}
 50-
 51-	return string(r)
 52-}
 53-
 54-func SanitizeFileExt(fname string) string {
 55-	return strings.TrimSuffix(fname, filepath.Ext(fname))
 56-}
 57-
 58-func KeyText(s ssh.Session) (string, error) {
 59-	if s.PublicKey() == nil {
 60-		return "", fmt.Errorf("Session doesn't have public key")
 61-	}
 62-	return KeyForKeyText(s.PublicKey())
 63-}
 64-
 65-func KeyForKeyText(pk ssh.PublicKey) (string, error) {
 66-	kb := base64.StdEncoding.EncodeToString(pk.Marshal())
 67-	return fmt.Sprintf("%s %s", pk.Type(), kb), nil
 68-}
 69-
 70-func KeyForSha256(pk ssh.PublicKey) string {
 71-	return gossh.FingerprintSHA256(pk)
 72-}
 73-
 74-func GetEnv(key string, defaultVal string) string {
 75-	if value, exists := os.LookupEnv(key); exists {
 76-		return value
 77-	}
 78-
 79-	return defaultVal
 80-}
 81-
 82-// IsText reports whether a significant prefix of s looks like correct UTF-8;
 83-// that is, if it is likely that s is human-readable text.
 84-func IsText(s string) bool {
 85-	const max = 1024 // at least utf8.UTFMax
 86-	if len(s) > max {
 87-		s = s[0:max]
 88-	}
 89-	for i, c := range s {
 90-		if i+utf8.UTFMax > len(s) {
 91-			// last char may be incomplete - ignore
 92-			break
 93-		}
 94-		if c == 0xFFFD || c < ' ' && c != '\n' && c != '\t' && c != '\f' && c != '\r' {
 95-			// decoding error or control character - not a text file
 96-			return false
 97-		}
 98-	}
 99-	return true
100-}
101-
102-func IsExtAllowed(filename string, allowedExt []string) bool {
103-	ext := pathpkg.Ext(filename)
104-	return slices.Contains(allowedExt, ext)
105-}
106-
107-// IsTextFile reports whether the file has a known extension indicating
108-// a text file, or if a significant chunk of the specified file looks like
109-// correct UTF-8; that is, if it is likely that the file contains human-
110-// readable text.
111-func IsTextFile(text string) bool {
112-	num := math.Min(float64(len(text)), 1024)
113-	return IsText(text[0:int(num)])
114-}
115-
116-const solarYearSecs = 31556926
117-
118-func TimeAgo(t *time.Time) string {
119-	d := time.Since(*t)
120-	var metric string
121-	var amount int
122-	if d.Seconds() < 60 {
123-		amount = int(d.Seconds())
124-		metric = "second"
125-	} else if d.Minutes() < 60 {
126-		amount = int(d.Minutes())
127-		metric = "minute"
128-	} else if d.Hours() < 24 {
129-		amount = int(d.Hours())
130-		metric = "hour"
131-	} else if d.Seconds() < solarYearSecs {
132-		amount = int(d.Hours()) / 24
133-		metric = "day"
134-	} else {
135-		amount = int(d.Seconds()) / solarYearSecs
136-		metric = "year"
137-	}
138-	if amount == 1 {
139-		return fmt.Sprintf("%d %s ago", amount, metric)
140-	} else {
141-		return fmt.Sprintf("%d %ss ago", amount, metric)
142-	}
143-}
144-
145-func Shasum(data []byte) string {
146-	h := sha256.New()
147-	h.Write(data)
148-	bs := h.Sum(nil)
149-	return hex.EncodeToString(bs)
150-}
151-
152-func BytesToMB(size int) float32 {
153-	return ((float32(size) / 1000) / 1000)
154-}
155-
156-func BytesToGB(size int) float32 {
157-	return BytesToMB(size) / 1000
158-}
159-
160-// https://stackoverflow.com/a/46964105
161-func StartOfMonth() time.Time {
162-	now := time.Now()
163-	firstday := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
164-	return firstday
165-}
166-
167-func StartOfYear() time.Time {
168-	now := time.Now()
169-	return now.AddDate(-1, 0, 0)
170-}
M tui/createaccount/create.go
+3, -6
 1@@ -8,8 +8,8 @@ import (
 2 	input "github.com/charmbracelet/bubbles/textinput"
 3 	tea "github.com/charmbracelet/bubbletea"
 4 	"github.com/picosh/pico/db"
 5-	"github.com/picosh/pico/shared"
 6 	"github.com/picosh/pico/tui/common"
 7+	"github.com/picosh/utils"
 8 )
 9 
10 type state int
11@@ -200,7 +200,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
12 // View renders current view from the model.
13 func (m Model) View() string {
14 	s := common.LogoView() + "\n\n"
15-	pubkey := fmt.Sprintf("pubkey: %s", shared.KeyForSha256(m.shared.Session.PublicKey()))
16+	pubkey := fmt.Sprintf("pubkey: %s", utils.KeyForSha256(m.shared.Session.PublicKey()))
17 	s += m.shared.Styles.Label.SetString(pubkey).String()
18 	s += "\n\n" + m.input.View() + "\n\n"
19 
20@@ -228,10 +228,7 @@ func (m *Model) createAccount() tea.Cmd {
21 			return NameInvalidMsg{}
22 		}
23 
24-		key, err := shared.KeyForKeyText(m.shared.Session.PublicKey())
25-		if err != nil {
26-			return errMsg{err}
27-		}
28+		key := utils.KeyForKeyText(m.shared.Session.PublicKey())
29 
30 		user, err := m.shared.Dbpool.RegisterUser(m.newName, key, "")
31 		if err != nil {
M tui/createkey/create.go
+3, -5
 1@@ -7,9 +7,9 @@ import (
 2 	input "github.com/charmbracelet/bubbles/textinput"
 3 	tea "github.com/charmbracelet/bubbletea"
 4 	"github.com/picosh/pico/db"
 5-	"github.com/picosh/pico/shared"
 6 	"github.com/picosh/pico/tui/common"
 7 	"github.com/picosh/pico/tui/pages"
 8+	"github.com/picosh/utils"
 9 	"golang.org/x/crypto/ssh"
10 )
11 
12@@ -239,10 +239,8 @@ func (m *Model) addPublicKey() tea.Cmd {
13 			return KeyInvalidMsg{}
14 		}
15 
16-		key, err := shared.KeyForKeyText(pk)
17-		if err != nil {
18-			return KeyInvalidMsg{}
19-		}
20+		key := utils.KeyForKeyText(pk)
21+
22 		err = m.shared.Dbpool.InsertPublicKey(m.shared.User.ID, key, comment, nil)
23 		if err != nil {
24 			if errors.Is(err, db.ErrPublicKeyTaken) {
M tui/logs/logs.go
+19, -10
 1@@ -11,9 +11,11 @@ import (
 2 	"github.com/charmbracelet/bubbles/viewport"
 3 	tea "github.com/charmbracelet/bubbletea"
 4 	"github.com/charmbracelet/lipgloss"
 5-	"github.com/picosh/pico/shared"
 6 	"github.com/picosh/pico/tui/common"
 7 	"github.com/picosh/pico/tui/pages"
 8+	"github.com/picosh/utils"
 9+
10+	pipeLogger "github.com/picosh/pubsub/log"
11 )
12 
13 type state int
14@@ -168,7 +170,14 @@ func (m Model) waitForActivity(sub chan map[string]any) tea.Cmd {
15 
16 func (m Model) connectLogs(sub chan map[string]any) tea.Cmd {
17 	return func() tea.Msg {
18-		stdoutPipe, err := shared.ConnectToLogs(m.ctx)
19+		stdoutPipe, err := pipeLogger.ConnectToLogs(m.ctx, &pipeLogger.PubSubConnectionInfo{
20+			RemoteHost:     utils.GetEnv("PICO_PIPE_ENDPOINT", "pipe.pico.sh:22"),
21+			KeyLocation:    utils.GetEnv("PICO_PIPE_KEY", "ssh_data/term_info_ed25519"),
22+			KeyPassphrase:  utils.GetEnv("PICO_PIPE_PASSPHRASE", ""),
23+			RemoteHostname: utils.GetEnv("PICO_PIPE_REMOTE_HOST", "pipe.pico.sh"),
24+			RemoteUser:     utils.GetEnv("PICO_PIPE_USER", "pico"),
25+		})
26+
27 		if err != nil {
28 			return errMsg(err)
29 		}
30@@ -184,7 +193,7 @@ func (m Model) connectLogs(sub chan map[string]any) tea.Cmd {
31 				continue
32 			}
33 
34-			user := shared.AnyToStr(parsedData, "user")
35+			user := utils.AnyToStr(parsedData, "user")
36 			if user == m.shared.User.Name {
37 				sub <- parsedData
38 			}
39@@ -201,13 +210,13 @@ func matched(str, match string) bool {
40 }
41 
42 func logToStr(styles common.Styles, data map[string]any, match string) string {
43-	time := shared.AnyToStr(data, "time")
44-	service := shared.AnyToStr(data, "service")
45-	level := shared.AnyToStr(data, "level")
46-	msg := shared.AnyToStr(data, "msg")
47-	errMsg := shared.AnyToStr(data, "err")
48-	status := shared.AnyToFloat(data, "status")
49-	url := shared.AnyToStr(data, "url")
50+	time := utils.AnyToStr(data, "time")
51+	service := utils.AnyToStr(data, "service")
52+	level := utils.AnyToStr(data, "level")
53+	msg := utils.AnyToStr(data, "msg")
54+	errMsg := utils.AnyToStr(data, "err")
55+	status := utils.AnyToFloat(data, "status")
56+	url := utils.AnyToStr(data, "url")
57 
58 	if match != "" {
59 		lvlMatch := matched(level, match)
M tui/settings/settings.go
+2, -2
 1@@ -8,9 +8,9 @@ import (
 2 	"github.com/charmbracelet/lipgloss"
 3 	"github.com/charmbracelet/lipgloss/table"
 4 	"github.com/picosh/pico/db"
 5-	"github.com/picosh/pico/shared"
 6 	"github.com/picosh/pico/tui/common"
 7 	"github.com/picosh/pico/tui/pages"
 8+	"github.com/picosh/utils"
 9 )
10 
11 var maxWidth = 50
12@@ -125,7 +125,7 @@ func (m Model) featuresView() string {
13 
14 	data := [][]string{}
15 	for _, feature := range m.features {
16-		storeMax := shared.BytesToGB(int(feature.FindStorageMax(0)))
17+		storeMax := utils.BytesToGB(int(feature.FindStorageMax(0)))
18 		row := []string{
19 			feature.Name,
20 			fmt.Sprintf("%.2f", storeMax),
M tui/util.go
+7, -5
 1@@ -2,10 +2,11 @@ package tui
 2 
 3 import (
 4 	"errors"
 5+	"fmt"
 6 
 7 	"github.com/picosh/pico/db"
 8-	"github.com/picosh/pico/shared"
 9 	"github.com/picosh/pico/tui/common"
10+	"github.com/picosh/utils"
11 )
12 
13 func findUser(shrd common.SharedModel) (*db.User, error) {
14@@ -13,12 +14,13 @@ func findUser(shrd common.SharedModel) (*db.User, error) {
15 	var user *db.User
16 	usr := shrd.Session.User()
17 
18-	key, err := shared.KeyForKeyText(shrd.Session.PublicKey())
19-	if err != nil {
20-		return nil, err
21+	if shrd.Session.PublicKey() == nil {
22+		return nil, fmt.Errorf("unable to find public key")
23 	}
24 
25-	user, err = shrd.Dbpool.FindUserForKey(usr, key)
26+	key := utils.KeyForKeyText(shrd.Session.PublicKey())
27+
28+	user, err := shrd.Dbpool.FindUserForKey(usr, key)
29 	if err != nil {
30 		logger.Error("no user found for public key", "err", err.Error())
31 		// we only want to throw an error for specific cases
M ui/api.go
+5, -10
 1@@ -12,6 +12,7 @@ import (
 2 	"github.com/charmbracelet/ssh"
 3 	"github.com/picosh/pico/db"
 4 	"github.com/picosh/pico/shared"
 5+	"github.com/picosh/utils"
 6 )
 7 
 8 type registerPayload struct {
 9@@ -35,13 +36,7 @@ func registerUser(apiConfig *shared.ApiConfig, ctx ssh.Context, pubkey ssh.Publi
10 		body, _ := io.ReadAll(r.Body)
11 		_ = json.Unmarshal(body, &payload)
12 
13-		pubkeyStr, err := shared.KeyForKeyText(pubkey)
14-		if err != nil {
15-			errMsg := fmt.Sprintf("could not get pubkey text: %s", err.Error())
16-			logger.Error("could not get pubkey text", "err", err.Error())
17-			shared.JSONError(w, errMsg, http.StatusUnprocessableEntity)
18-			return
19-		}
20+		pubkeyStr := utils.KeyForKeyText(pubkey)
21 
22 		user, err := dbpool.RegisterUser(payload.Name, pubkeyStr, "")
23 		if err != nil {
24@@ -130,7 +125,7 @@ func toFingerprint(pubkey string) (string, error) {
25 	if err != nil {
26 		return "", err
27 	}
28-	return shared.KeyForSha256(kk), nil
29+	return utils.KeyForSha256(kk), nil
30 }
31 
32 func getPublicKeys(httpCtx *shared.ApiConfig, ctx ssh.Context) http.HandlerFunc {
33@@ -636,8 +631,8 @@ func getAnalytics(apiConfig *shared.ApiConfig, ctx ssh.Context, sumtype, bytype,
34 			by = "post_id"
35 		}
36 
37-		year := &db.SummaryOpts{FkID: fkID, By: by, Interval: "month", Origin: shared.StartOfYear(), Where: where}
38-		month := &db.SummaryOpts{FkID: fkID, By: by, Interval: "day", Origin: shared.StartOfMonth(), Where: where}
39+		year := &db.SummaryOpts{FkID: fkID, By: by, Interval: "month", Origin: utils.StartOfYear(), Where: where}
40+		month := &db.SummaryOpts{FkID: fkID, By: by, Interval: "day", Origin: utils.StartOfMonth(), Where: where}
41 
42 		opts := year
43 		if sumtype == "month" {