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