repos / pico

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

commit
b4d4114
parent
0b5edcf
author
Eric Bower
date
2023-08-04 01:05:50 +0000 UTC
feat: cannot link to a project that is also a link

A project linked to another project which is also linked to a
project is forbidden.  CI/CD Example:
        - ProjectProd links to ProjectStaging
        - ProjectStaging links to ProjectMain
        - We merge `main` and trigger a deploy which uploads to ProjectMain
        - All three get updated immediately
This scenario was not the intent of our CI/CD.  What we actually
wanted was to create a snapshot of ProjectMain and have ProjectStaging
link to the snapshot, but that's not the intended design of pgs.

So we want to close that gap here.
12 files changed,  +157, -99
M cmd/pgs/ssh/main.go
+1, -1
1@@ -38,7 +38,7 @@ func createRouter(cfg *shared.ConfigSite, handler *uploadassets.UploadAssetHandl
2 		return []wish.Middleware{
3 			pipe.Middleware(handler, ""),
4 			list.Middleware(handler),
5-			pgs.WishMiddleware(handler.DBPool, handler.Storage, handler.Cfg.Logger),
6+			pgs.WishMiddleware(handler),
7 			scp.Middleware(handler),
8 			wishrsync.Middleware(handler),
9 			auth.Middleware(handler),
M db/db.go
+1, -1
1@@ -207,7 +207,7 @@ type DB interface {
2 	FindFeedItemsByPostID(postID string) ([]*FeedItem, error)
3 
4 	InsertProject(userID, name, projectDir string) (string, error)
5-	UpdateProject(projectID, projectDir string) error
6+	LinkToProject(userID, projectID, projectDir string) error
7 	RemoveProject(projectID string) error
8 	FindProjectByName(userID, name string) (*Project, error)
9 	FindProjectsByUser(userID string) ([]*Project, error)
M db/postgres/storage.go
+34, -4
 1@@ -246,7 +246,7 @@ const (
 2 	sqlInsertProject      = `INSERT INTO projects (user_id, name, project_dir) VALUES ($1, $2, $3) RETURNING id;`
 3 	sqlFindProjectByName  = `SELECT id, user_id, name, project_dir FROM projects WHERE user_id = $1 AND name = $2;`
 4 	sqlFindProjectsByUser = `SELECT id, user_id, name, project_dir FROM projects WHERE user_id = $1;`
 5-	sqlUpdateProject      = `UPDATE projects SET project_dir = $1, updated_at = $2 WHERE id = $3;`
 6+	sqlLinkToProject      = `UPDATE projects SET project_dir = $1, updated_at = $2 WHERE id = $3;`
 7 	sqlRemoveProject      = `DELETE FROM projects WHERE id = $1;`
 8 )
 9 
10@@ -1189,9 +1189,39 @@ func (me *PsqlDB) InsertProject(userID, name, projectDir string) (string, error)
11 	return id, nil
12 }
13 
14-func (me *PsqlDB) UpdateProject(projectID, projectDir string) error {
15-	_, err := me.Db.Exec(
16-		sqlUpdateProject,
17+func (me *PsqlDB) LinkToProject(userID, projectID, projectDir string) error {
18+	linkToProject, err := me.FindProjectByName(userID, projectDir)
19+	if err != nil {
20+		return err
21+	}
22+
23+	/*
24+		A project linked to another project which is also linked to a
25+		project is forbidden.  CI/CD Example:
26+			- ProjectProd links to ProjectStaging
27+			- ProjectStaging links to ProjectMain
28+			- We merge `main` and trigger a deploy which uploads to ProjectMain
29+			- All three get updated immediately
30+		This scenario was not the intent of our CI/CD.  What we actually
31+		wanted was to create a snapshot of ProjectMain and have ProjectStaging
32+		link to the snapshot, but that's not the intended design of pgs.
33+
34+		So we want to close that gap here.
35+
36+		We ensure that `project.Name` and `project.ProjectDir` are identical
37+		when there is no aliasing.
38+	*/
39+	if linkToProject.Name != linkToProject.ProjectDir {
40+		return fmt.Errorf(
41+			"cannot link (%s) to (%s) because it is also a link to (%s)",
42+			projectID,
43+			projectDir,
44+			linkToProject.ProjectDir,
45+		)
46+	}
47+
48+	_, err = me.Db.Exec(
49+		sqlLinkToProject,
50 		projectDir,
51 		time.Now(),
52 		projectID,
M filehandlers/assets/asset.go
+10, -23
 1@@ -11,23 +11,9 @@ import (
 2 )
 3 
 4 func (h *UploadAssetHandler) validateAsset(data *FileData) (bool, error) {
 5-	assetBucket := shared.GetAssetBucketName(data.User.ID)
 6-	bucket, err := h.Storage.UpsertBucket(assetBucket)
 7-	if err != nil {
 8-		return false, err
 9-	}
10-	totalFileSize, err := h.Storage.GetBucketQuota(bucket)
11-	if err != nil {
12-		return false, err
13-	}
14-
15 	fname := filepath.Base(data.Filepath)
16-	if int(data.Size) > maxAssetSize {
17-		return false, fmt.Errorf("ERROR: file (%s) has exceeded maximum file size (%d bytes)", fname, maxAssetSize)
18-	}
19-
20-	if totalFileSize+uint64(data.Size) > uint64(maxSize) {
21-		return false, fmt.Errorf("ERROR: user (%s) has exceeded (%d bytes) max (%d bytes)", data.User.Name, totalFileSize, maxSize)
22+	if int(data.Size) > h.Cfg.MaxAssetSize {
23+		return false, fmt.Errorf("ERROR: file (%s) has exceeded maximum file size (%d bytes)", fname, h.Cfg.MaxAssetSize)
24 	}
25 
26 	if !shared.IsExtAllowed(fname, h.Cfg.AllowedExt) {
27@@ -49,22 +35,23 @@ func (h *UploadAssetHandler) writeAsset(data *FileData) error {
28 		return err
29 	}
30 
31-	assetBucket := shared.GetAssetBucketName(data.User.ID)
32 	assetFilename := shared.GetAssetFileName(data.FileEntry)
33-	bucket, err := h.Storage.UpsertBucket(assetBucket)
34-	if err != nil {
35-		return err
36-	}
37 
38 	if data.Size == 0 {
39-		err = h.Storage.DeleteFile(bucket, assetFilename)
40+		err = h.Storage.DeleteFile(data.Bucket, assetFilename)
41 		if err != nil {
42 			return err
43 		}
44 	} else {
45 		reader := bytes.NewReader(data.Text)
46+		h.Cfg.Logger.Infof(
47+			"(%s) uploading to (bucket: %s) (%s)",
48+			data.User.Name,
49+			data.Bucket.Name,
50+			assetFilename,
51+		)
52 		_, err := h.Storage.PutFile(
53-			bucket,
54+			data.Bucket,
55 			assetFilename,
56 			storage.NopReaderAtCloser(reader),
57 		)
M filehandlers/assets/handler.go
+48, -34
  1@@ -4,8 +4,10 @@ import (
  2 	"encoding/binary"
  3 	"fmt"
  4 	"io"
  5+	"net/url"
  6 	"os"
  7 	"path/filepath"
  8+	"strings"
  9 	"time"
 10 
 11 	"github.com/gliderlabs/ssh"
 12@@ -16,18 +18,22 @@ import (
 13 	"github.com/picosh/pico/wish/send/utils"
 14 )
 15 
 16-var KB = 1024
 17-var MB = KB * 1024
 18-var GB = MB * 1024
 19-var maxSize = 1 * GB
 20-var maxAssetSize = 50 * MB
 21+type ctxUserKey struct{}
 22 
 23-func bytesToGB(size int) float32 {
 24-	return (((float32(size) / 1024) / 1024) / 1024)
 25+func getAssetURL(c *shared.ConfigSite, username, projectName, dir, slug string) string {
 26+	fname := url.PathEscape(strings.TrimLeft(slug, "/"))
 27+	fdir := strings.TrimLeft(dir, "/")
 28+	return fmt.Sprintf(
 29+		"%s://%s-%s.%s/%s/%s",
 30+		c.Protocol,
 31+		username,
 32+		projectName,
 33+		c.Domain,
 34+		fdir,
 35+		fname,
 36+	)
 37 }
 38 
 39-type ctxUserKey struct{}
 40-
 41 func getUser(s ssh.Session) (*db.User, error) {
 42 	user := s.Context().Value(ctxUserKey{}).(*db.User)
 43 	if user == nil {
 44@@ -38,8 +44,9 @@ func getUser(s ssh.Session) (*db.User, error) {
 45 
 46 type FileData struct {
 47 	*utils.FileEntry
 48-	Text []byte
 49-	User *db.User
 50+	Text   []byte
 51+	User   *db.User
 52+	Bucket storage.Bucket
 53 }
 54 
 55 type UploadAssetHandler struct {
 56@@ -137,8 +144,24 @@ func (h *UploadAssetHandler) Validate(s ssh.Session) error {
 57 		return fmt.Errorf("you do not have access to this service")
 58 	}
 59 
 60+	assetBucket := shared.GetAssetBucketName(user.ID)
 61+	bucket, err := h.Storage.UpsertBucket(assetBucket)
 62+	if err != nil {
 63+		return err
 64+	}
 65+	totalFileSize, err := h.Storage.GetBucketQuota(bucket)
 66+	if err != nil {
 67+		return err
 68+	}
 69+
 70+	h.Cfg.Logger.Infof("(%s) bucket size is current (%d bytes)", user.Name, totalFileSize)
 71+	if totalFileSize >= uint64(h.Cfg.MaxSize) {
 72+		return fmt.Errorf("ERROR: user (%s) has exceeded (%d bytes) max (%d bytes)", user.Name, totalFileSize, h.Cfg.MaxSize)
 73+	}
 74+
 75 	s.Context().SetValue(ctxUserKey{}, user)
 76 	h.Cfg.Logger.Infof("(%s) attempting to upload files to (%s)", user.Name, h.Cfg.Space)
 77+
 78 	return nil
 79 }
 80 
 81@@ -157,10 +180,17 @@ func (h *UploadAssetHandler) Write(s ssh.Session, entry *utils.FileEntry) (strin
 82 	// filesize from sftp,scp,rsync
 83 	entry.Size = int64(fileSize)
 84 
 85+	assetBucket := shared.GetAssetBucketName(user.ID)
 86+	bucket, err := h.Storage.UpsertBucket(assetBucket)
 87+	if err != nil {
 88+		return "", err
 89+	}
 90+
 91 	data := &FileData{
 92 		FileEntry: entry,
 93 		User:      user,
 94 		Text:      origText,
 95+		Bucket:    bucket,
 96 	}
 97 	err = h.writeAsset(data)
 98 	if err != nil {
 99@@ -178,29 +208,13 @@ func (h *UploadAssetHandler) Write(s ssh.Session, entry *utils.FileEntry) (strin
100 		}
101 	}
102 
103-	bucketName := shared.GetAssetBucketName(user.ID)
104-	bucket, err := h.Storage.UpsertBucket(bucketName)
105-	if err != nil {
106-		return "", err
107-	}
108-
109-	totalFileSize, err := h.Storage.GetBucketQuota(bucket)
110-	if err != nil {
111-		return "", err
112-	}
113-
114-	curl := shared.NewCreateURL(h.Cfg)
115-	url := h.Cfg.FullPostURL(
116-		curl,
117-		fmt.Sprintf("%s-%s", user.Name, projectName),
118+	url := getAssetURL(
119+		h.Cfg,
120+		user.Name,
121+		projectName,
122+		filepath.Dir(strings.Replace(data.Filepath, "/"+projectName, "", 1)),
123 		filepath.Base(data.Filepath),
124 	)
125-	str := fmt.Sprintf(
126-		"%s (space: %.2f/%.2fGB, %.2f%%)",
127-		url,
128-		bytesToGB(int(totalFileSize)),
129-		bytesToGB(maxSize),
130-		(float32(totalFileSize)/float32(maxSize))*100,
131-	)
132-	return str, nil
133+
134+	return url, nil
135 }
M filehandlers/imgs/handler.go
+4, -11
 1@@ -19,15 +19,8 @@ import (
 2 	"golang.org/x/exp/slices"
 3 )
 4 
 5-var KB = 1024
 6-var MB = KB * 1024
 7-var GB = MB * 1024
 8-var maxSize = 1 * GB
 9-var maxImgSize = 10 * MB
10-
11-func bytesToGB(size int) float32 {
12-	return (((float32(size) / 1024) / 1024) / 1024)
13-}
14+var maxSize = 1 * shared.GB
15+var maxImgSize = 10 * shared.MB
16 
17 type ctxUserKey struct{}
18 
19@@ -274,8 +267,8 @@ func (h *UploadImgHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
20 	str := fmt.Sprintf(
21 		"%s (space: %.2f/%.2fGB, %.2f%%)",
22 		url,
23-		bytesToGB(totalFileSize),
24-		bytesToGB(maxSize),
25+		shared.BytesToGB(totalFileSize),
26+		shared.BytesToGB(maxSize),
27 		(float32(totalFileSize)/float32(maxSize))*100,
28 	)
29 	return str, nil
M filehandlers/imgs/img.go
+1, -1
1@@ -68,7 +68,7 @@ func (h *UploadImgHandler) metaImg(data *PostMetaData) error {
2 	opt := shared.NewImgOptimizer(h.Cfg.Logger, "")
3 	// for small images we want to preserve quality
4 	// since it can have a dramatic effect
5-	if data.FileSize < 3*MB {
6+	if data.FileSize < 3*shared.MB {
7 		opt.Quality = 100
8 		opt.Lossless = true
9 	} else {
M pgs/config.go
+4, -18
 1@@ -7,24 +7,8 @@ import (
 2 	"github.com/picosh/pico/wish/cms/config"
 3 )
 4 
 5-type ImgsLinkify struct {
 6-	Cfg          *shared.ConfigSite
 7-	Username     string
 8-	OnSubdomain  bool
 9-	WithUsername bool
10-}
11-
12-func NewImgsLinkify(username string) *ImgsLinkify {
13-	cfg := NewConfigSite()
14-	return &ImgsLinkify{
15-		Cfg:      cfg,
16-		Username: username,
17-	}
18-}
19-
20-func (i *ImgsLinkify) Create(fname string) string {
21-	return i.Cfg.ImgFullURL(i.Username, fname)
22-}
23+var maxSize = 1 * shared.GB
24+var maxAssetSize = 50 * shared.MB
25 
26 func NewConfigSite() *shared.ConfigSite {
27 	debug := shared.GetEnv("PGS_DEBUG", "0")
28@@ -84,6 +68,8 @@ func NewConfigSite() *shared.ConfigSite {
29 				".json",
30 				".md",
31 			},
32+			MaxSize:       maxSize,
33+			MaxAssetSize:  maxAssetSize,
34 			Logger:        shared.CreateLogger(),
35 			AllowRegister: allowRegister == "1",
36 		},
M pgs/html/marketing.page.tmpl
+1, -1
1@@ -38,7 +38,7 @@
2     <h2 class="text-lg font-bold">Examples</h2>
3     <ul>
4       <li>The site you are reading right now</li>
5-      <li><a href="https://git.erock.sh">git web viewer</a></li>
6+      <li><a href="https://git.erock.io">git web viewer</a></li>
7     </ul>
8   </section>
9 
M pgs/wish.go
+43, -5
 1@@ -7,11 +7,10 @@ import (
 2 	"github.com/charmbracelet/wish"
 3 	"github.com/gliderlabs/ssh"
 4 	"github.com/picosh/pico/db"
 5+	uploadassets "github.com/picosh/pico/filehandlers/assets"
 6 	"github.com/picosh/pico/shared"
 7-	"github.com/picosh/pico/shared/storage"
 8 	"github.com/picosh/pico/wish/cms/util"
 9 	"github.com/picosh/pico/wish/send/utils"
10-	"go.uber.org/zap"
11 )
12 
13 func getUser(s ssh.Session, dbpool db.DB) (*db.User, error) {
14@@ -44,7 +43,12 @@ func getHelpText(userName, projectName string) string {
15 	return helpStr
16 }
17 
18-func WishMiddleware(dbpool db.DB, store storage.ObjectStorage, log *zap.SugaredLogger) wish.Middleware {
19+func WishMiddleware(handler *uploadassets.UploadAssetHandler) wish.Middleware {
20+	dbpool := handler.DBPool
21+	log := handler.Cfg.Logger
22+	cfg := handler.Cfg
23+	store := handler.Storage
24+
25 	return func(sshHandler ssh.Handler) ssh.Handler {
26 		return func(session ssh.Session) {
27 			_, _, activePty := session.Pty()
28@@ -65,7 +69,41 @@ func WishMiddleware(dbpool db.DB, store storage.ObjectStorage, log *zap.SugaredL
29 				cmd := strings.TrimSpace(args[0])
30 				if cmd == "help" {
31 					_, _ = session.Write([]byte(getHelpText(user.Name, "projectA")))
32-				} else if cmd == "list" {
33+				} else if cmd == "stats" {
34+					bucketName := shared.GetAssetBucketName(user.ID)
35+					bucket, err := store.UpsertBucket(bucketName)
36+					if err != nil {
37+						log.Error(err)
38+						utils.ErrorHandler(session, err)
39+						return
40+					}
41+
42+					totalFileSize, err := store.GetBucketQuota(bucket)
43+					if err != nil {
44+						log.Error(err)
45+						utils.ErrorHandler(session, err)
46+						return
47+					}
48+
49+					projects, err := dbpool.FindProjectsByUser(user.ID)
50+					if err != nil {
51+						log.Error(err)
52+						utils.ErrorHandler(session, err)
53+						return
54+					}
55+
56+					str := "stats\n"
57+					str += "=====\n"
58+					str += fmt.Sprintf(
59+						"space:\t\t%.4f/%.2fGB, %.2f%%\n",
60+						shared.BytesToGB(int(totalFileSize)),
61+						shared.BytesToGB(cfg.MaxSize),
62+						(float32(totalFileSize)/float32(cfg.MaxSize))*100,
63+					)
64+					str += fmt.Sprintf("projects:\t%d\n", len(projects))
65+					_, _ = session.Write([]byte(str))
66+					return
67+				} else if cmd == "list" || cmd == "ls" {
68 					projects, err := dbpool.FindProjectsByUser(user.ID)
69 					if err != nil {
70 						log.Error(err)
71@@ -125,7 +163,7 @@ func WishMiddleware(dbpool db.DB, store storage.ObjectStorage, log *zap.SugaredL
72 				project, err := dbpool.FindProjectByName(user.ID, projectName)
73 				if err == nil {
74 					log.Infof("user (%s) already has project (%s), updating ...", user.Name, projectName)
75-					err = dbpool.UpdateProject(project.ID, projectDir)
76+					err = dbpool.LinkToProject(user.ID, project.ID, projectDir)
77 					if err != nil {
78 						log.Error(err)
79 						utils.ErrorHandler(session, err)
M shared/util.go
+8, -0
 1@@ -21,6 +21,10 @@ import (
 2 
 3 var fnameRe = regexp.MustCompile(`[-_]+`)
 4 
 5+var KB = 1024
 6+var MB = KB * 1024
 7+var GB = MB * 1024
 8+
 9 func FilenameToTitle(filename string, title string) string {
10 	if filename != title {
11 		return title
12@@ -162,3 +166,7 @@ func GetMimeType(fpath string) string {
13 
14 	return "text/plain"
15 }
16+
17+func BytesToGB(size int) float32 {
18+	return (((float32(size) / 1024) / 1024) / 1024)
19+}
M wish/cms/config/config.go
+2, -0
1@@ -26,6 +26,8 @@ type ConfigCms struct {
2 	HiddenPosts   []string
3 	Logger        *zap.SugaredLogger
4 	AllowRegister bool
5+	MaxSize       int
6+	MaxAssetSize  int
7 }
8 
9 func NewConfigCms() *ConfigCms {