repos / pico

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

commit
d3e2409
parent
e6c6e2f
author
Eric Bower
date
2025-01-18 08:14:31 -0500 EST
chore: rm imgs
11 files changed,  +0, -969
M .env.example
+0, -19
 1@@ -66,25 +66,6 @@ PROSE_DOMAIN=prose.dev.pico.sh:3002
 2 PROSE_PROTOCOL=http
 3 PROSE_DEBUG=1
 4 
 5-IMGS_CADDYFILE=./caddy/Caddyfile
 6-IMGS_V4=
 7-IMGS_V6=
 8-IMGS_HTTP_V4=$IMGS_V4:80
 9-IMGS_HTTP_V6=[$IMGS_V6]:80
10-IMGS_HTTPS_V4=$IMGS_V4:443
11-IMGS_HTTPS_V6=[$IMGS_V6]:443
12-IMGS_SSH_V4=$IMGS_V4:22
13-IMGS_SSH_V6=[$IMGS_V6]:22
14-IMGS_HOST=
15-IMGS_SSH_PORT=2222
16-IMGS_WEB_PORT=3000
17-IMGS_PROM_PORT=9222
18-IMGS_DOMAIN=imgs.dev.pico.sh:3003
19-IMGS_EMAIL=hello@pico.sh
20-IMGS_PROTOCOL=http
21-IMGS_STORAGE_DIR=.storage
22-IMGS_DEBUG=1
23-
24 SENDGRID_API_KEY=
25 FEEDS_CADDYFILE=./caddy/Caddyfile
26 FEEDS_V4=
D cmd/imgs/ssh/main.go
+0, -9
 1@@ -1,9 +0,0 @@
 2-package main
 3-
 4-import (
 5-	"github.com/picosh/pico/imgs"
 6-)
 7-
 8-func main() {
 9-	imgs.StartSshServer()
10-}
D cmd/imgs/web/main.go
+0, -7
1@@ -1,7 +0,0 @@
2-package main
3-
4-import "github.com/picosh/pico/imgs"
5-
6-func main() {
7-	imgs.StartApiServer()
8-}
M docker-compose.override.yml
+0, -30
 1@@ -11,13 +11,6 @@ services:
 2     ports:
 3       - "9000:9000"
 4       - "9001:9001"
 5-  registry:
 6-    env_file:
 7-      - .env.example
 8-    ports:
 9-      - "5000:5000"
10-    volumes:
11-      - ./imgs/registry.yml:/etc/docker/registry/config.yml
12   imgproxy:
13     env_file:
14       - .env.example
15@@ -87,29 +80,6 @@ services:
16       - ./data/prose-ssh/data:/app/ssh_data
17     ports:
18       - "2222:2222"
19-  imgs-web:
20-    build:
21-      args:
22-        APP: imgs
23-      target: release-web
24-    env_file:
25-      - .env.example
26-    volumes:
27-      - ./data/storage/data:/app/.storage
28-    ports:
29-      - "3003:3000"
30-  imgs-ssh:
31-    build:
32-      args:
33-        APP: imgs
34-      target: release-ssh
35-    env_file:
36-      - .env.example
37-    volumes:
38-      - ./data/storage/data:/app/.storage
39-      - ./data/imgs-ssh/data:/app/ssh_data
40-    ports:
41-      - "2223:2222"
42   pgs-web:
43     build:
44       args:
M docker-compose.prod.yml
+0, -60
 1@@ -35,13 +35,6 @@ services:
 2       - .env.prod
 3     volumes:
 4       - ./data/pipemgr/data/term_info_ed25519:/key:ro
 5-  registry:
 6-    env_file:
 7-      - .env.prod
 8-    volumes:
 9-      - ./imgs/registry.yml:/etc/docker/registry/config.yml
10-    networks:
11-      - imgs
12   imgproxy:
13     env_file:
14       - .env.prod
15@@ -186,53 +179,6 @@ services:
16     ports:
17       - "${PROSE_SSH_V4:-22}:2222"
18       - "${PROSE_SSH_V6:-[::1]:22}:2222"
19-  imgs-caddy:
20-    image: ghcr.io/picosh/pico/caddy:latest
21-    restart: always
22-    networks:
23-      - imgs
24-    env_file:
25-      - .env.prod
26-    environment:
27-      APP_DOMAIN: ${IMGS_DOMAIN:-imgs.sh}
28-      APP_EMAIL: ${IMGS_EMAIL:-hello@pico.sh}
29-    volumes:
30-      - ${IMGS_CADDYFILE}:/etc/caddy/Caddyfile
31-      - ./data/imgs-caddy/data:/data
32-      - ./data/imgs-caddy/config:/config
33-    ports:
34-      - "${IMGS_HTTPS_V4:-443}:443"
35-      - "${IMGS_HTTP_V4:-80}:80"
36-      - "${IMGS_HTTPS_V6:-[::1]:443}:443"
37-      - "${IMGS_HTTP_V6:-[::1]:80}:80"
38-    profiles:
39-      - imgs
40-      - caddy
41-      - all
42-  imgs-web:
43-    networks:
44-      imgs:
45-        aliases:
46-          - web
47-    env_file:
48-      - .env.prod
49-    volumes:
50-      - ./data/storage/data:/app/.storage
51-      - ./data/imgs-ssh/data:/app/ssh_data
52-  imgs-ssh:
53-    networks:
54-      imgs:
55-        aliases:
56-          - ssh
57-    env_file:
58-      - .env.prod
59-    volumes:
60-      - ./data/storage/data:/app/.storage
61-      - ./data/imgs-ssh/data:/app/ssh_data
62-      - ./data/imgs-tmp:/tmp
63-    ports:
64-      - "${IMGS_SSH_V4:-22}:2222"
65-      - "${IMGS_SSH_V6:-[::1]:22}:2222"
66   pgs-caddy:
67     image: ghcr.io/picosh/pico/caddy:latest
68     restart: always
69@@ -392,12 +338,6 @@ networks:
70     ipam:
71       config:
72         - subnet: 172.19.0.0/16
73-  imgs:
74-    driver_opts:
75-      com.docker.network.bridge.name: imgs
76-    ipam:
77-      config:
78-        - subnet: 172.21.0.0/16
79   feeds:
80     driver_opts:
81       com.docker.network.bridge.name: feeds
M docker-compose.yml
+0, -24
 1@@ -12,16 +12,6 @@ services:
 2     profiles:
 3       - db
 4       - all
 5-  registry:
 6-    image: registry
 7-    restart: always
 8-    profiles:
 9-      - imgs
10-      - services
11-      - all
12-    environment:
13-      REGISTRY_STORAGE_S3_ACCESSKEY: ${MINIO_ROOT_USER}
14-      REGISTRY_STORAGE_S3_SECRETKEY: ${MINIO_ROOT_PASSWORD}
15   imgproxy:
16     image: darthsim/imgproxy:latest
17     restart: always
18@@ -85,20 +75,6 @@ services:
19       - prose
20       - services
21       - all
22-  imgs-web:
23-    image: ghcr.io/picosh/pico/imgs-web:latest
24-    restart: always
25-    profiles:
26-      - imgs
27-      - services
28-      - all
29-  imgs-ssh:
30-    image: ghcr.io/picosh/pico/imgs-ssh:latest
31-    restart: always
32-    profiles:
33-      - imgs
34-      - services
35-      - all
36   pgs-web:
37     image: ghcr.io/picosh/pico/pgs-web:latest
38     restart: always
M imgs/api.go
+0, -181
  1@@ -1,19 +1,13 @@
  2 package imgs
  3 
  4 import (
  5-	"bytes"
  6 	"fmt"
  7 	"html/template"
  8 	"net/http"
  9 	"net/url"
 10 	"path/filepath"
 11-	"time"
 12 
 13-	_ "net/http/pprof"
 14-
 15-	"github.com/gorilla/feeds"
 16 	"github.com/picosh/pico/db"
 17-	"github.com/picosh/pico/db/postgres"
 18 	"github.com/picosh/pico/pgs"
 19 	"github.com/picosh/pico/shared"
 20 	"github.com/picosh/pico/shared/storage"
 21@@ -90,82 +84,6 @@ func ImgsListHandler(w http.ResponseWriter, r *http.Request) {
 22 	}
 23 }
 24 
 25-func ImgsRssHandler(w http.ResponseWriter, r *http.Request) {
 26-	dbpool := shared.GetDB(r)
 27-	logger := shared.GetLogger(r)
 28-	cfg := shared.GetCfg(r)
 29-
 30-	pager, err := dbpool.FindAllPosts(&db.Pager{Num: 25, Page: 0}, Space)
 31-	if err != nil {
 32-		logger.Error(err.Error())
 33-		http.Error(w, err.Error(), http.StatusInternalServerError)
 34-		return
 35-	}
 36-
 37-	ts, err := template.ParseFiles(cfg.StaticPath("html/rss.page.tmpl"))
 38-	if err != nil {
 39-		logger.Error(err.Error())
 40-		http.Error(w, err.Error(), http.StatusInternalServerError)
 41-		return
 42-	}
 43-
 44-	feed := &feeds.Feed{
 45-		Title:       fmt.Sprintf("%s imgs feed", cfg.Domain),
 46-		Link:        &feeds.Link{Href: cfg.HomeURL()},
 47-		Description: fmt.Sprintf("%s latest image", cfg.Domain),
 48-		Author:      &feeds.Author{Name: cfg.Domain},
 49-		Created:     time.Now(),
 50-	}
 51-
 52-	curl := shared.CreateURLFromRequest(cfg, r)
 53-
 54-	var feedItems []*feeds.Item
 55-	for _, post := range pager.Data {
 56-		var tpl bytes.Buffer
 57-		data := &PostPageData{
 58-			ImgURL: template.URL(cfg.ImgURL(curl, post.Username, post.Filename)),
 59-		}
 60-		if err := ts.Execute(&tpl, data); err != nil {
 61-			continue
 62-		}
 63-
 64-		realUrl := cfg.FullPostURL(curl, post.Username, post.Filename)
 65-		if !curl.Subdomain && !curl.UsernameInRoute {
 66-			realUrl = fmt.Sprintf("%s://%s%s", cfg.Protocol, r.Host, realUrl)
 67-		}
 68-
 69-		item := &feeds.Item{
 70-			Id:          realUrl,
 71-			Title:       post.Title,
 72-			Link:        &feeds.Link{Href: realUrl},
 73-			Content:     tpl.String(),
 74-			Created:     *post.PublishAt,
 75-			Updated:     *post.UpdatedAt,
 76-			Description: post.Description,
 77-			Author:      &feeds.Author{Name: post.Username},
 78-		}
 79-
 80-		if post.Description != "" {
 81-			item.Description = post.Description
 82-		}
 83-
 84-		feedItems = append(feedItems, item)
 85-	}
 86-	feed.Items = feedItems
 87-
 88-	rss, err := feed.ToAtom()
 89-	if err != nil {
 90-		logger.Error(err.Error())
 91-		http.Error(w, "Could not generate atom rss feed", http.StatusInternalServerError)
 92-	}
 93-
 94-	w.Header().Add("Content-Type", "application/atom+xml")
 95-	_, err = w.Write([]byte(rss))
 96-	if err != nil {
 97-		logger.Error(err.Error())
 98-	}
 99-}
100-
101 func anyPerm(proj *db.Project) bool {
102 	return true
103 }
104@@ -248,102 +166,3 @@ func FindImgPost(r *http.Request, user *db.User, slug string) (*db.Post, error)
105 	dbpool := shared.GetDB(r)
106 	return dbpool.FindPostWithSlug(slug, user.ID, Space)
107 }
108-
109-func redirectHandler(w http.ResponseWriter, r *http.Request) {
110-	username := shared.GetUsernameFromRequest(r)
111-	url := fmt.Sprintf("https://%s.prose.sh/i", username)
112-	http.Redirect(w, r, url, http.StatusMovedPermanently)
113-}
114-
115-func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
116-	routes := []shared.Route{
117-		shared.NewRoute("GET", "/check", shared.CheckHandler),
118-		shared.NewRoute("GET", "/", func(w http.ResponseWriter, r *http.Request) {
119-			http.Redirect(w, r, "https://prose.sh", http.StatusMovedPermanently)
120-		}),
121-	}
122-
123-	routes = append(
124-		routes,
125-		staticRoutes...,
126-	)
127-
128-	routes = append(
129-		routes,
130-		shared.NewRoute("GET", "/rss", ImgsRssHandler),
131-		shared.NewRoute("GET", "/rss.xml", ImgsRssHandler),
132-		shared.NewRoute("GET", "/atom.xml", ImgsRssHandler),
133-		shared.NewRoute("GET", "/feed.xml", ImgsRssHandler),
134-
135-		shared.NewRoute("GET", "/([^/]+)", redirectHandler),
136-		shared.NewRoute("GET", "/([^/]+)/o/([^/]+)", ImgRequest),
137-		shared.NewRoute("GET", "/([^/]+)/([^/]+)", ImgRequest),
138-		shared.NewRoute("GET", "/([^/]+)/([^/]+)/(.+)", ImgRequest),
139-	)
140-
141-	return routes
142-}
143-
144-func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
145-	routes := []shared.Route{}
146-
147-	routes = append(
148-		routes,
149-		staticRoutes...,
150-	)
151-
152-	routes = append(
153-		routes,
154-		shared.NewRoute("GET", "/", redirectHandler),
155-		shared.NewRoute("GET", "/o/([^/]+)", ImgRequest),
156-		shared.NewRoute("GET", "/([^/]+)", ImgRequest),
157-		shared.NewRoute("GET", "/([^/]+)/(.+)", ImgRequest),
158-	)
159-
160-	return routes
161-}
162-
163-func StartApiServer() {
164-	cfg := NewConfigSite()
165-	logger := cfg.Logger
166-
167-	db := postgres.NewDB(cfg.DbURL, cfg.Logger)
168-	defer db.Close()
169-
170-	var st storage.StorageServe
171-	var err error
172-	if cfg.MinioURL == "" {
173-		st, err = storage.NewStorageFS(cfg.Logger, cfg.StorageDir)
174-	} else {
175-		st, err = storage.NewStorageMinio(cfg.Logger, cfg.MinioURL, cfg.MinioUser, cfg.MinioPass)
176-	}
177-
178-	if err != nil {
179-		logger.Error(err.Error())
180-	}
181-
182-	staticRoutes := []shared.Route{}
183-	if cfg.Debug {
184-		staticRoutes = shared.CreatePProfRoutes(staticRoutes)
185-	}
186-
187-	mainRoutes := createMainRoutes(staticRoutes)
188-	subdomainRoutes := createSubdomainRoutes(staticRoutes)
189-
190-	apiConfig := &shared.ApiConfig{
191-		Cfg:     cfg,
192-		Dbpool:  db,
193-		Storage: st,
194-	}
195-	handler := shared.CreateServe(mainRoutes, subdomainRoutes, apiConfig)
196-	router := http.HandlerFunc(handler)
197-
198-	portStr := fmt.Sprintf(":%s", cfg.Port)
199-	logger.Info(
200-		"Starting server on port",
201-		"port", cfg.Port,
202-		"domain", cfg.Domain,
203-	)
204-
205-	logger.Error(http.ListenAndServe(portStr, router).Error())
206-}
D imgs/cli.go
+0, -275
  1@@ -1,275 +0,0 @@
  2-package imgs
  3-
  4-import (
  5-	"encoding/json"
  6-	"flag"
  7-	"fmt"
  8-	"io"
  9-	"log/slog"
 10-	"net/http"
 11-	"path/filepath"
 12-	"strings"
 13-
 14-	"github.com/charmbracelet/ssh"
 15-	"github.com/charmbracelet/wish"
 16-	"github.com/google/uuid"
 17-	"github.com/picosh/pico/db"
 18-	"github.com/picosh/pico/shared/storage"
 19-	"github.com/picosh/pico/tui/common"
 20-	sst "github.com/picosh/pobj/storage"
 21-	psub "github.com/picosh/pubsub"
 22-	sendutils "github.com/picosh/send/utils"
 23-	"github.com/picosh/utils"
 24-)
 25-
 26-func getUser(s ssh.Session, dbpool db.DB) (*db.User, error) {
 27-	if s.PublicKey() == nil {
 28-		return nil, fmt.Errorf("key not found")
 29-	}
 30-
 31-	key := utils.KeyForKeyText(s.PublicKey())
 32-
 33-	user, err := dbpool.FindUserForKey(s.User(), key)
 34-	if err != nil {
 35-		return nil, err
 36-	}
 37-
 38-	if user.Name == "" {
 39-		return nil, fmt.Errorf("must have username set")
 40-	}
 41-
 42-	return user, nil
 43-}
 44-
 45-func flagSet(cmdName string, sesh ssh.Session) (*flag.FlagSet, *bool) {
 46-	cmd := flag.NewFlagSet(cmdName, flag.ContinueOnError)
 47-	cmd.SetOutput(sesh)
 48-	write := cmd.Bool("write", false, "apply changes")
 49-	return cmd, write
 50-}
 51-
 52-func flagCheck(cmd *flag.FlagSet, posArg string, cmdArgs []string) bool {
 53-	_ = cmd.Parse(cmdArgs)
 54-
 55-	if posArg == "-h" || posArg == "--help" || posArg == "-help" {
 56-		cmd.Usage()
 57-		return false
 58-	}
 59-	return true
 60-}
 61-
 62-type Cmd struct {
 63-	User        *db.User
 64-	Session     utils.CmdSession
 65-	Log         *slog.Logger
 66-	Dbpool      db.DB
 67-	Write       bool
 68-	Styles      common.Styles
 69-	Storage     sst.ObjectStorage
 70-	RegistryUrl string
 71-}
 72-
 73-func (c *Cmd) output(out string) {
 74-	_, _ = c.Session.Write([]byte(out + "\r\n"))
 75-}
 76-
 77-func (c *Cmd) error(err error) {
 78-	_, _ = fmt.Fprint(c.Session.Stderr(), err, "\r\n")
 79-	_ = c.Session.Exit(1)
 80-	_ = c.Session.Close()
 81-}
 82-
 83-func (c *Cmd) bail(err error) {
 84-	if err == nil {
 85-		return
 86-	}
 87-	c.Log.Error(err.Error())
 88-	c.error(err)
 89-}
 90-
 91-func (c *Cmd) notice() {
 92-	if !c.Write {
 93-		c.output("\nNOTICE: changes not commited, use `--write` to save operation")
 94-	}
 95-}
 96-
 97-func (c *Cmd) help() {
 98-	helpStr := "Commands: [help, ls, rm]\n"
 99-	helpStr += "NOTICE: *must* append with `--write` for the changes to persist.\n"
100-	c.output(helpStr)
101-}
102-
103-func (c *Cmd) rm(repo string) error {
104-	bucket, err := c.Storage.GetBucket("imgs")
105-	if err != nil {
106-		return err
107-	}
108-
109-	fp := filepath.Join("docker/registry/v2/repositories", c.User.Name, repo)
110-
111-	fileList, err := c.Storage.ListObjects(bucket, fp, true)
112-	if err != nil {
113-		return err
114-	}
115-
116-	if len(fileList) == 0 {
117-		c.output(fmt.Sprintf("repo not found (%s)", repo))
118-		return nil
119-	}
120-	c.output(fmt.Sprintf("found (%d) objects for repo (%s), removing", len(fileList), repo))
121-
122-	for _, obj := range fileList {
123-		fname := filepath.Join(fp, obj.Name())
124-		intent := fmt.Sprintf("deleted (%s)", obj.Name())
125-		c.Log.Info(
126-			"attempting to delete file",
127-			"user", c.User.Name,
128-			"bucket", bucket.Name,
129-			"repo", repo,
130-			"filename", fname,
131-		)
132-		if c.Write {
133-			err := c.Storage.DeleteObject(bucket, fname)
134-			if err != nil {
135-				return err
136-			}
137-		}
138-		c.output(intent)
139-	}
140-
141-	return nil
142-}
143-
144-type RegistryCatalog struct {
145-	Repos []string `json:"repositories"`
146-}
147-
148-func (c *Cmd) ls() error {
149-	res, err := http.Get(
150-		fmt.Sprintf("http://%s/v2/_catalog", c.RegistryUrl),
151-	)
152-	if err != nil {
153-		return err
154-	}
155-
156-	body, err := io.ReadAll(res.Body)
157-	if err != nil {
158-		return err
159-	}
160-
161-	var data RegistryCatalog
162-	err = json.Unmarshal(body, &data)
163-
164-	if err != nil {
165-		return err
166-	}
167-
168-	if len(data.Repos) == 0 {
169-		c.output("You don't have any repos on imgs.sh")
170-		return nil
171-	}
172-
173-	user := c.User.Name
174-	out := "repos\n"
175-	out += "-----\n"
176-	for _, repo := range data.Repos {
177-		if !strings.HasPrefix(repo, user+"/") {
178-			continue
179-		}
180-		rr := strings.TrimPrefix(repo, user+"/")
181-		out += fmt.Sprintf("%s\n", rr)
182-	}
183-	c.output(out)
184-	return nil
185-}
186-
187-type CliHandler struct {
188-	DBPool      db.DB
189-	Logger      *slog.Logger
190-	Storage     storage.StorageServe
191-	RegistryUrl string
192-	PubSub      psub.PubSub
193-}
194-
195-func WishMiddleware(handler *CliHandler) wish.Middleware {
196-	dbpool := handler.DBPool
197-	log := handler.Logger
198-	st := handler.Storage
199-	pubsub := handler.PubSub
200-
201-	return func(next ssh.Handler) ssh.Handler {
202-		return func(sesh ssh.Session) {
203-			user, err := getUser(sesh, dbpool)
204-			if err != nil {
205-				sendutils.ErrorHandler(sesh, err)
206-				return
207-			}
208-
209-			args := sesh.Command()
210-
211-			opts := Cmd{
212-				Session:     sesh,
213-				User:        user,
214-				Log:         log,
215-				Dbpool:      dbpool,
216-				Write:       false,
217-				Storage:     st,
218-				RegistryUrl: handler.RegistryUrl,
219-			}
220-
221-			if len(args) == 0 {
222-				next(sesh)
223-				return
224-			}
225-
226-			cmd := strings.TrimSpace(args[0])
227-			if len(args) == 1 {
228-				if cmd == "help" {
229-					opts.help()
230-					return
231-				} else if cmd == "ls" {
232-					err := opts.ls()
233-					opts.bail(err)
234-					return
235-				} else {
236-					next(sesh)
237-					return
238-				}
239-			}
240-
241-			repoName := strings.TrimSpace(args[1])
242-			cmdArgs := args[2:]
243-			log.Info(
244-				"imgs middleware detected command",
245-				"args", args,
246-				"cmd", cmd,
247-				"repoName", repoName,
248-				"cmdArgs", cmdArgs,
249-			)
250-
251-			if cmd == "rm" {
252-				rmCmd, write := flagSet("rm", sesh)
253-				if !flagCheck(rmCmd, repoName, cmdArgs) {
254-					return
255-				}
256-				opts.Write = *write
257-
258-				err := opts.rm(repoName)
259-				opts.notice()
260-				opts.bail(err)
261-				return
262-			} else if cmd == "sub" {
263-				err = pubsub.Sub(sesh.Context(), uuid.NewString(), sesh, []*psub.Channel{
264-					psub.NewChannel(fmt.Sprintf("%s/%s", user.Name, repoName)),
265-				}, false)
266-
267-				if err != nil {
268-					wish.Errorln(sesh, err)
269-				}
270-			} else {
271-				next(sesh)
272-				return
273-			}
274-		}
275-	}
276-}
D imgs/config.go
+0, -35
 1@@ -1,35 +0,0 @@
 2-package imgs
 3-
 4-import (
 5-	"github.com/picosh/pico/shared"
 6-	"github.com/picosh/utils"
 7-)
 8-
 9-func NewConfigSite() *shared.ConfigSite {
10-	debug := utils.GetEnv("IMGS_DEBUG", "0")
11-	domain := utils.GetEnv("IMGS_DOMAIN", "prose.sh")
12-	port := utils.GetEnv("IMGS_WEB_PORT", "3000")
13-	protocol := utils.GetEnv("IMGS_PROTOCOL", "https")
14-	storageDir := utils.GetEnv("IMGS_STORAGE_DIR", ".storage")
15-	minioURL := utils.GetEnv("MINIO_URL", "")
16-	minioUser := utils.GetEnv("MINIO_ROOT_USER", "")
17-	minioPass := utils.GetEnv("MINIO_ROOT_PASSWORD", "")
18-	dbURL := utils.GetEnv("DATABASE_URL", "")
19-
20-	cfg := shared.ConfigSite{
21-		Debug:      debug == "1",
22-		Domain:     domain,
23-		Port:       port,
24-		Protocol:   protocol,
25-		DbURL:      dbURL,
26-		StorageDir: storageDir,
27-		MinioURL:   minioURL,
28-		MinioUser:  minioUser,
29-		MinioPass:  minioPass,
30-		Space:      "imgs",
31-		AllowedExt: []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico"},
32-		Logger:     shared.CreateLogger("imgs"),
33-	}
34-
35-	return &cfg
36-}
D imgs/registry.yml
+0, -11
 1@@ -1,11 +0,0 @@
 2-version: 0.1
 3-http:
 4-  addr: :5000
 5-storage:
 6-  s3:
 7-    region: us-east-1
 8-    bucket: imgs
 9-    # regionendpoint: http://minio:9000
10-    regionendpoint: https://minio.pico.sh
11-  redirect:
12-    disable: true
D imgs/ssh.go
+0, -318
  1@@ -1,318 +0,0 @@
  2-package imgs
  3-
  4-import (
  5-	"bytes"
  6-	"context"
  7-	"encoding/json"
  8-	"fmt"
  9-	"io"
 10-	"log"
 11-	"log/slog"
 12-	"net/http"
 13-	"net/http/httputil"
 14-	"net/url"
 15-	"os"
 16-	"os/signal"
 17-	"strconv"
 18-	"strings"
 19-	"syscall"
 20-	"time"
 21-
 22-	"github.com/charmbracelet/promwish"
 23-	"github.com/charmbracelet/ssh"
 24-	"github.com/charmbracelet/wish"
 25-	"github.com/google/uuid"
 26-	"github.com/picosh/pico/db"
 27-	"github.com/picosh/pico/db/postgres"
 28-	"github.com/picosh/pico/shared"
 29-	"github.com/picosh/pico/shared/storage"
 30-	wsh "github.com/picosh/pico/wish"
 31-	psub "github.com/picosh/pubsub"
 32-	"github.com/picosh/tunkit"
 33-	"github.com/picosh/utils"
 34-)
 35-
 36-type ctxUserKey struct{}
 37-
 38-func getUserCtx(ctx ssh.Context) (*db.User, error) {
 39-	user, ok := ctx.Value(ctxUserKey{}).(*db.User)
 40-	if user == nil || !ok {
 41-		return user, fmt.Errorf("user not set on `ssh.Context()` for connection")
 42-	}
 43-	return user, nil
 44-}
 45-func setUserCtx(ctx ssh.Context, user *db.User) {
 46-	ctx.SetValue(ctxUserKey{}, user)
 47-}
 48-
 49-func AuthHandler(dbh db.DB, log *slog.Logger) func(ssh.Context, ssh.PublicKey) bool {
 50-	return func(ctx ssh.Context, key ssh.PublicKey) bool {
 51-		kk := utils.KeyForKeyText(key)
 52-
 53-		user, err := dbh.FindUserForKey("", kk)
 54-		if err != nil {
 55-			log.Error("user not found", "err", err)
 56-			return false
 57-		}
 58-
 59-		if user == nil {
 60-			log.Error("user not found", "err", err)
 61-			return false
 62-		}
 63-
 64-		setUserCtx(ctx, user)
 65-
 66-		if !dbh.HasFeatureForUser(user.ID, "plus") {
 67-			log.Error("not a pico+ user", "user", user.Name)
 68-			return false
 69-		}
 70-
 71-		return true
 72-	}
 73-}
 74-
 75-type ErrorHandler struct {
 76-	Err error
 77-}
 78-
 79-func (e *ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 80-	log.Println(e.Err.Error())
 81-	http.Error(w, e.Err.Error(), http.StatusInternalServerError)
 82-}
 83-
 84-func createServeMux(handler *CliHandler, pubsub psub.PubSub) func(ctx ssh.Context) http.Handler {
 85-	return func(ctx ssh.Context) http.Handler {
 86-		router := http.NewServeMux()
 87-
 88-		slug := ""
 89-		user, err := getUserCtx(ctx)
 90-		if err == nil && user != nil {
 91-			slug = user.Name
 92-		}
 93-
 94-		proxy := httputil.NewSingleHostReverseProxy(&url.URL{
 95-			Scheme: "http",
 96-			Host:   handler.RegistryUrl,
 97-		})
 98-
 99-		oldDirector := proxy.Director
100-
101-		proxy.Director = func(r *http.Request) {
102-			handler.Logger.Info("director", "request", r)
103-			oldDirector(r)
104-
105-			if strings.HasSuffix(r.URL.Path, "_catalog") || r.URL.Path == "/v2" || r.URL.Path == "/v2/" {
106-				return
107-			}
108-
109-			fullPath := strings.TrimPrefix(r.URL.Path, "/v2")
110-
111-			newPath, err := url.JoinPath("/v2", slug, fullPath)
112-			if err != nil {
113-				return
114-			}
115-
116-			r.URL.Path = newPath
117-
118-			query := r.URL.Query()
119-
120-			if query.Has("from") {
121-				joinedFrom, err := url.JoinPath(slug, query.Get("from"))
122-				if err != nil {
123-					return
124-				}
125-				query.Set("from", joinedFrom)
126-
127-				r.URL.RawQuery = query.Encode()
128-			}
129-		}
130-
131-		proxy.ModifyResponse = func(r *http.Response) error {
132-			handler.Logger.Info("modify", "request", r)
133-			shared.CorsHeaders(r.Header)
134-
135-			if r.Request.Method == http.MethodGet && strings.HasSuffix(r.Request.URL.Path, "_catalog") {
136-				b, err := io.ReadAll(r.Body)
137-				if err != nil {
138-					return err
139-				}
140-
141-				err = r.Body.Close()
142-				if err != nil {
143-					return err
144-				}
145-
146-				var data map[string]any
147-				err = json.Unmarshal(b, &data)
148-				if err != nil {
149-					return err
150-				}
151-
152-				newRepos := []string{}
153-
154-				if repos, ok := data["repositories"].([]any); ok {
155-					for _, repo := range repos {
156-						if repoStr, ok := repo.(string); ok && strings.HasPrefix(repoStr, slug) {
157-							newRepos = append(newRepos, strings.Replace(repoStr, fmt.Sprintf("%s/", slug), "", 1))
158-						}
159-					}
160-				}
161-
162-				data["repositories"] = newRepos
163-
164-				newB, err := json.Marshal(data)
165-				if err != nil {
166-					return err
167-				}
168-
169-				jsonBuf := bytes.NewBuffer(newB)
170-
171-				r.ContentLength = int64(jsonBuf.Len())
172-				r.Header.Set("Content-Length", strconv.FormatInt(r.ContentLength, 10))
173-				r.Body = io.NopCloser(jsonBuf)
174-			}
175-
176-			if r.Request.Method == http.MethodGet && (strings.Contains(r.Request.URL.Path, "/tags/") || strings.Contains(r.Request.URL.Path, "/manifests/")) {
177-				splitPath := strings.Split(r.Request.URL.Path, "/")
178-
179-				if len(splitPath) > 1 {
180-					ele := splitPath[len(splitPath)-2]
181-					if ele == "tags" || ele == "manifests" {
182-						b, err := io.ReadAll(r.Body)
183-						if err != nil {
184-							return err
185-						}
186-
187-						err = r.Body.Close()
188-						if err != nil {
189-							return err
190-						}
191-
192-						var data map[string]any
193-						err = json.Unmarshal(b, &data)
194-						if err != nil {
195-							return err
196-						}
197-
198-						if name, ok := data["name"].(string); ok {
199-							if strings.HasPrefix(name, slug) {
200-								data["name"] = strings.Replace(name, fmt.Sprintf("%s/", slug), "", 1)
201-							}
202-						}
203-
204-						newB, err := json.Marshal(data)
205-						if err != nil {
206-							return err
207-						}
208-
209-						jsonBuf := bytes.NewBuffer(newB)
210-
211-						r.ContentLength = int64(jsonBuf.Len())
212-						r.Header.Set("Content-Length", strconv.FormatInt(r.ContentLength, 10))
213-						r.Body = io.NopCloser(jsonBuf)
214-					}
215-				}
216-			}
217-
218-			if r.Request.Method == http.MethodPut && strings.Contains(r.Request.URL.Path, "/manifests/") {
219-				digest := r.Header.Get("Docker-Content-Digest")
220-				// [ ]/v2/erock/alpine/manifests/latest
221-				splitPath := strings.Split(r.Request.URL.Path, "/")
222-				img := splitPath[3]
223-				tag := splitPath[5]
224-
225-				furl := fmt.Sprintf(
226-					"digest=%s&image=%s&tag=%s",
227-					url.QueryEscape(digest),
228-					img,
229-					tag,
230-				)
231-				handler.Logger.Info("sending event", "url", furl)
232-
233-				err := pubsub.Pub(ctx, uuid.NewString(), bytes.NewBufferString(furl), []*psub.Channel{
234-					psub.NewChannel(fmt.Sprintf("%s/%s:%s", user.Name, img, tag)),
235-				}, false)
236-
237-				if err != nil {
238-					handler.Logger.Error("pub error", "err", err)
239-				}
240-			}
241-
242-			locationHeader := r.Header.Get("location")
243-			if strings.Contains(locationHeader, fmt.Sprintf("/v2/%s", slug)) {
244-				r.Header.Set("location", strings.ReplaceAll(locationHeader, fmt.Sprintf("/v2/%s", slug), "/v2"))
245-			}
246-
247-			return nil
248-		}
249-
250-		router.HandleFunc("/", proxy.ServeHTTP)
251-
252-		return router
253-	}
254-}
255-
256-func StartSshServer() {
257-	host := utils.GetEnv("IMGS_HOST", "0.0.0.0")
258-	port := utils.GetEnv("IMGS_SSH_PORT", "2222")
259-	promPort := utils.GetEnv("IMGS_PROM_PORT", "9222")
260-	dbUrl := os.Getenv("DATABASE_URL")
261-	registryUrl := utils.GetEnv("REGISTRY_URL", "0.0.0.0:5000")
262-	minioUrl := utils.GetEnv("MINIO_URL", "http://0.0.0.0:9000")
263-	minioUser := utils.GetEnv("MINIO_ROOT_USER", "")
264-	minioPass := utils.GetEnv("MINIO_ROOT_PASSWORD", "")
265-
266-	logger := shared.CreateLogger("imgs")
267-	logger.Info("bootup", "registry", registryUrl, "minio", minioUrl)
268-	dbh := postgres.NewDB(dbUrl, logger)
269-	st, err := storage.NewStorageMinio(logger, minioUrl, minioUser, minioPass)
270-	if err != nil {
271-		panic(err)
272-	}
273-
274-	pubsub := psub.NewMulticast(logger)
275-	handler := &CliHandler{
276-		Logger:      logger,
277-		DBPool:      dbh,
278-		Storage:     st,
279-		RegistryUrl: registryUrl,
280-		PubSub:      pubsub,
281-	}
282-
283-	s, err := wish.NewServer(
284-		wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
285-		wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
286-		wish.WithPublicKeyAuth(AuthHandler(dbh, logger)),
287-		wish.WithMiddleware(
288-			WishMiddleware(handler),
289-			promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "imgs-ssh"),
290-			wsh.LogMiddleware(logger),
291-		),
292-		tunkit.WithWebTunnel(
293-			tunkit.NewWebTunnelHandler(createServeMux(handler, pubsub), logger),
294-		),
295-	)
296-
297-	if err != nil {
298-		logger.Error("could not create server", "err", err)
299-	}
300-
301-	done := make(chan os.Signal, 1)
302-	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
303-	logger.Info("starting SSH server", "host", host, "port", port)
304-	go func() {
305-		if err = s.ListenAndServe(); err != nil {
306-			logger.Error("serve error", "err", err)
307-			os.Exit(1)
308-		}
309-	}()
310-
311-	<-done
312-	logger.Info("stopping SSH server")
313-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
314-	defer func() { cancel() }()
315-	if err := s.Shutdown(ctx); err != nil {
316-		logger.Error("shutdown", "err", err)
317-		os.Exit(1)
318-	}
319-}