- 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
+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)
+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,
+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 )
+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 }
+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
+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 {
+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 },
+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
+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)
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+}
+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 {