repos / pico

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

commit
0b65298
parent
a872b1d
author
Eric Bower
date
2023-09-06 02:29:20 +0000 UTC
feat(pgs): `/rss` endpoint
5 files changed,  +142, -30
M db/db.go
+7, -4
 1@@ -32,10 +32,12 @@ type PostData struct {
 2 }
 3 
 4 type Project struct {
 5-	ID         string `json:"id"`
 6-	UserID     string `json:"user_id"`
 7-	Name       string `json:"name"`
 8-	ProjectDir string `json:"project_dir"`
 9+	ID         string     `json:"id"`
10+	UserID     string     `json:"user_id"`
11+	Name       string     `json:"name"`
12+	ProjectDir string     `json:"project_dir"`
13+	Username   string     `json:"username"`
14+	CreatedAt  *time.Time `json:"created_at"`
15 }
16 
17 // Make the Attrs struct implement the driver.Valuer interface. This method
18@@ -214,6 +216,7 @@ type DB interface {
19 	FindProjectLinks(userID, name string) ([]*Project, error)
20 	FindProjectsByUser(userID string) ([]*Project, error)
21 	FindProjectsByPrefix(userID, name string) ([]*Project, error)
22+	FindAllProjects(page *Pager) (*Paginate[*Project], error)
23 
24 	Close() error
25 }
M db/postgres/storage.go
+51, -3
 1@@ -244,9 +244,16 @@ const (
 2 	sqlRemoveKeys          = `DELETE FROM public_keys WHERE id = ANY($1::uuid[])`
 3 	sqlRemoveUsers         = `DELETE FROM app_users WHERE id = ANY($1::uuid[])`
 4 
 5-	sqlInsertProject        = `INSERT INTO projects (user_id, name, project_dir) VALUES ($1, $2, $3) RETURNING id;`
 6-	sqlUpdateProject        = `UPDATE projects SET updated_at = $3 WHERE user_id = $1 AND name = $2;`
 7-	sqlFindProjectByName    = `SELECT id, user_id, name, project_dir FROM projects WHERE user_id = $1 AND name = $2;`
 8+	sqlInsertProject      = `INSERT INTO projects (user_id, name, project_dir) VALUES ($1, $2, $3) RETURNING id;`
 9+	sqlUpdateProject      = `UPDATE projects SET updated_at = $3 WHERE user_id = $1 AND name = $2;`
10+	sqlFindProjectByName  = `SELECT id, user_id, name, project_dir FROM projects WHERE user_id = $1 AND name = $2;`
11+	sqlSelectProjectCount = `SELECT count(id) FROM projects`
12+	sqlFindAllProjects    = `
13+	SELECT projects.id, user_id, app_users.name as username, projects.name, project_dir, projects.created_at
14+	FROM projects
15+	LEFT OUTER JOIN app_users ON app_users.id = projects.user_id
16+	ORDER BY created_at ASC
17+	LIMIT $1 OFFSET $2`
18 	sqlFindProjectsByUser   = `SELECT id, user_id, name, project_dir FROM projects WHERE user_id = $1 ORDER BY name ASC;`
19 	sqlFindProjectsByPrefix = `SELECT id, user_id, name, project_dir FROM projects WHERE user_id = $1 AND name = project_dir AND name ILIKE $2 ORDER BY updated_at ASC, name ASC;`
20 	sqlFindProjectLinks     = `SELECT id, user_id, name, project_dir FROM projects WHERE user_id = $1 AND name != project_dir AND project_dir = $2 ORDER BY name ASC;`
21@@ -1350,3 +1357,44 @@ func (me *PsqlDB) FindProjectsByUser(userID string) ([]*db.Project, error) {
22 
23 	return projects, nil
24 }
25+
26+func (me *PsqlDB) FindAllProjects(page *db.Pager) (*db.Paginate[*db.Project], error) {
27+	var projects []*db.Project
28+	rs, err := me.Db.Query(sqlFindAllProjects, page.Num, page.Num*page.Page)
29+	if err != nil {
30+		return nil, err
31+	}
32+	for rs.Next() {
33+		project := &db.Project{}
34+		err := rs.Scan(
35+			&project.ID,
36+			&project.UserID,
37+			&project.Username,
38+			&project.Name,
39+			&project.ProjectDir,
40+			&project.CreatedAt,
41+		)
42+		if err != nil {
43+			return nil, err
44+		}
45+
46+		projects = append(projects, project)
47+	}
48+
49+	if rs.Err() != nil {
50+		return nil, rs.Err()
51+	}
52+
53+	var count int
54+	err = me.Db.QueryRow(sqlSelectProjectCount).Scan(&count)
55+	if err != nil {
56+		return nil, err
57+	}
58+
59+	pager := &db.Paginate[*db.Project]{
60+		Data:  projects,
61+		Total: int(math.Ceil(float64(count) / float64(page.Num))),
62+	}
63+
64+	return pager, nil
65+}
M filehandlers/assets/handler.go
+1, -23
 1@@ -22,27 +22,6 @@ type ctxBucketKey struct{}
 2 type ctxBucketQuotaKey struct{}
 3 type ctxProjectKey struct{}
 4 
 5-func getAssetURL(c *shared.ConfigSite, username, projectName, fpath string) string {
 6-	if username == projectName {
 7-		return fmt.Sprintf(
 8-			"%s://%s.%s/%s",
 9-			c.Protocol,
10-			username,
11-			c.Domain,
12-			fpath,
13-		)
14-	}
15-
16-	return fmt.Sprintf(
17-		"%s://%s-%s.%s/%s",
18-		c.Protocol,
19-		username,
20-		projectName,
21-		c.Domain,
22-		fpath,
23-	)
24-}
25-
26 func getProject(s ssh.Session) *db.Project {
27 	v := s.Context().Value(ctxProjectKey{})
28 	if v == nil {
29@@ -252,8 +231,7 @@ func (h *UploadAssetHandler) Write(s ssh.Session, entry *utils.FileEntry) (strin
30 		return "", err
31 	}
32 
33-	url := getAssetURL(
34-		h.Cfg,
35+	url := h.Cfg.AssetURL(
36 		user.Name,
37 		projectName,
38 		strings.Replace(data.Filepath, "/"+projectName+"/", "", 1),
M pgs/api.go
+62, -0
 1@@ -2,6 +2,7 @@ package pgs
 2 
 3 import (
 4 	"fmt"
 5+	"html/template"
 6 	"io"
 7 	"net/http"
 8 	"net/url"
 9@@ -11,6 +12,7 @@ import (
10 
11 	_ "net/http/pprof"
12 
13+	"github.com/gorilla/feeds"
14 	gocache "github.com/patrickmn/go-cache"
15 	"github.com/picosh/pico/db"
16 	"github.com/picosh/pico/db/postgres"
17@@ -75,6 +77,65 @@ func checkHandler(w http.ResponseWriter, r *http.Request) {
18 	w.WriteHeader(http.StatusNotFound)
19 }
20 
21+type RssData struct {
22+	Contents template.HTML
23+}
24+
25+func rssHandler(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.FindAllProjects(&db.Pager{Num: 50, Page: 0})
31+	if err != nil {
32+		logger.Error(err)
33+		http.Error(w, err.Error(), http.StatusInternalServerError)
34+		return
35+	}
36+
37+	feed := &feeds.Feed{
38+		Title:       fmt.Sprintf("%s discovery feed", cfg.Domain),
39+		Link:        &feeds.Link{Href: cfg.ReadURL()},
40+		Description: fmt.Sprintf("%s latest projects", cfg.Domain),
41+		Author:      &feeds.Author{Name: cfg.Domain},
42+		Created:     time.Now(),
43+	}
44+
45+	var feedItems []*feeds.Item
46+	for _, project := range pager.Data {
47+		realUrl := strings.TrimSuffix(
48+			cfg.AssetURL(project.Username, project.Name, ""),
49+			"/",
50+		)
51+
52+		item := &feeds.Item{
53+			Id:          realUrl,
54+			Title:       project.Name,
55+			Link:        &feeds.Link{Href: realUrl},
56+			Content:     fmt.Sprintf(`<a href="%s">%s</a>`, realUrl, realUrl),
57+			Created:     *project.CreatedAt,
58+			Updated:     *project.CreatedAt,
59+			Description: "",
60+			Author:      &feeds.Author{Name: project.Username},
61+		}
62+
63+		feedItems = append(feedItems, item)
64+	}
65+	feed.Items = feedItems
66+
67+	rss, err := feed.ToAtom()
68+	if err != nil {
69+		logger.Fatal(err)
70+		http.Error(w, "Could not generate atom rss feed", http.StatusInternalServerError)
71+	}
72+
73+	w.Header().Add("Content-Type", "application/atom+xml")
74+	_, err = w.Write([]byte(rss))
75+	if err != nil {
76+		logger.Error(err)
77+	}
78+}
79+
80 func calcPossibleRoutes(projectName, fp string) []string {
81 	fname := filepath.Base(fp)
82 	fdir := filepath.Dir(fp)
83@@ -237,6 +298,7 @@ func StartApiServer() {
84 	mainRoutes := []shared.Route{
85 		shared.NewRoute("GET", "/", marketingRequest),
86 		shared.NewRoute("GET", "/check", checkHandler),
87+		shared.NewRoute("GET", "/rss", rssHandler),
88 		shared.NewRoute("GET", "/(.+)", marketingRequest),
89 	}
90 	subdomainRoutes := []shared.Route{
M shared/config.go
+21, -0
 1@@ -227,6 +227,27 @@ func (c *ConfigSite) TagURL(curl *CreateURL, username, tag string) string {
 2 	return fmt.Sprintf("%s?tag=%s", c.FullBlogURL(curl, username), tg)
 3 }
 4 
 5+func (c *ConfigSite) AssetURL(username, projectName, fpath string) string {
 6+	if username == projectName {
 7+		return fmt.Sprintf(
 8+			"%s://%s.%s/%s",
 9+			c.Protocol,
10+			username,
11+			c.Domain,
12+			fpath,
13+		)
14+	}
15+
16+	return fmt.Sprintf(
17+		"%s://%s-%s.%s/%s",
18+		c.Protocol,
19+		username,
20+		projectName,
21+		c.Domain,
22+		fpath,
23+	)
24+}
25+
26 func CreateLogger() *zap.SugaredLogger {
27 	logger, err := zap.NewProduction()
28 	if err != nil {