repos / pico

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

commit
da7212a
parent
e65a196
author
Eric Bower
date
2022-07-30 02:01:45 +0000 UTC
refactor: abstract scp upload handler
16 files changed,  +229, -334
M cmd/lists/ssh/main.go
+10, -5
 1@@ -8,6 +8,7 @@ import (
 2 	"syscall"
 3 	"time"
 4 
 5+	"git.sr.ht/~erock/pico/filehandlers"
 6 	"git.sr.ht/~erock/pico/lists"
 7 	"git.sr.ht/~erock/pico/shared"
 8 	"git.sr.ht/~erock/pico/wish/cms"
 9@@ -27,7 +28,7 @@ func (me *SSHServer) authHandler(ctx ssh.Context, key ssh.PublicKey) bool {
10 	return true
11 }
12 
13-func createRouter(handler *lists.DbHandler) proxy.Router {
14+func createRouter(handler *filehandlers.ScpUploadHandler) proxy.Router {
15 	return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
16 		cmd := s.Command()
17 		mdw := []wish.Middleware{}
18@@ -45,7 +46,7 @@ func createRouter(handler *lists.DbHandler) proxy.Router {
19 	}
20 }
21 
22-func withProxy(handler *lists.DbHandler) ssh.Option {
23+func withProxy(handler *filehandlers.ScpUploadHandler) ssh.Option {
24 	return func(server *ssh.Server) error {
25 		err := sftp.SSHOption(handler)(server)
26 		if err != nil {
27@@ -57,13 +58,17 @@ func withProxy(handler *lists.DbHandler) ssh.Option {
28 }
29 
30 func main() {
31-	host := shared.GetEnv("PROSE_HOST", "0.0.0.0")
32-	port := shared.GetEnv("PROSE_SSH_PORT", "2222")
33+	host := shared.GetEnv("LISTS_HOST", "0.0.0.0")
34+	port := shared.GetEnv("LISTS_SSH_PORT", "2222")
35 	cfg := lists.NewConfigSite()
36 	logger := cfg.Logger
37 	dbh := postgres.NewDB(&cfg.ConfigCms)
38 	defer dbh.Close()
39-	handler := lists.NewDbHandler(dbh, cfg)
40+
41+	fileHandler := lists.ListsHandler{
42+		Cfg: cfg,
43+	}
44+	handler := filehandlers.NewScpPostHandler(dbh, cfg, &fileHandler)
45 
46 	sshServer := &SSHServer{}
47 	s, err := wish.NewServer(
M cmd/pastes/ssh/main.go
+9, -5
 1@@ -8,6 +8,7 @@ import (
 2 	"syscall"
 3 	"time"
 4 
 5+	"git.sr.ht/~erock/pico/filehandlers"
 6 	"git.sr.ht/~erock/pico/pastes"
 7 	"git.sr.ht/~erock/pico/shared"
 8 	"git.sr.ht/~erock/pico/wish/cms"
 9@@ -27,7 +28,7 @@ func (me *SSHServer) authHandler(ctx ssh.Context, key ssh.PublicKey) bool {
10 	return true
11 }
12 
13-func createRouter(handler *pastes.DbHandler) proxy.Router {
14+func createRouter(handler *filehandlers.ScpUploadHandler) proxy.Router {
15 	return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
16 		cmd := s.Command()
17 		mdw := []wish.Middleware{}
18@@ -45,7 +46,7 @@ func createRouter(handler *pastes.DbHandler) proxy.Router {
19 	}
20 }
21 
22-func withProxy(handler *pastes.DbHandler) ssh.Option {
23+func withProxy(handler *filehandlers.ScpUploadHandler) ssh.Option {
24 	return func(server *ssh.Server) error {
25 		err := sftp.SSHOption(handler)(server)
26 		if err != nil {
27@@ -57,13 +58,16 @@ func withProxy(handler *pastes.DbHandler) ssh.Option {
28 }
29 
30 func main() {
31-	host := shared.GetEnv("PROSE_HOST", "0.0.0.0")
32-	port := shared.GetEnv("PROSE_SSH_PORT", "2222")
33+	host := shared.GetEnv("PASTES_HOST", "0.0.0.0")
34+	port := shared.GetEnv("PASTES_SSH_PORT", "2222")
35 	cfg := pastes.NewConfigSite()
36 	logger := cfg.Logger
37 	dbh := postgres.NewDB(&cfg.ConfigCms)
38 	defer dbh.Close()
39-	handler := pastes.NewDbHandler(dbh, cfg)
40+	fileHandler := pastes.PastesHandler{
41+		Cfg: cfg,
42+	}
43+	handler := filehandlers.NewScpPostHandler(dbh, cfg, &fileHandler)
44 
45 	sshServer := &SSHServer{}
46 	s, err := wish.NewServer(
M cmd/prose/ssh/main.go
+7, -3
 1@@ -8,6 +8,7 @@ import (
 2 	"syscall"
 3 	"time"
 4 
 5+	"git.sr.ht/~erock/pico/filehandlers"
 6 	"git.sr.ht/~erock/pico/prose"
 7 	"git.sr.ht/~erock/pico/shared"
 8 	"git.sr.ht/~erock/pico/wish/cms"
 9@@ -27,7 +28,7 @@ func (me *SSHServer) authHandler(ctx ssh.Context, key ssh.PublicKey) bool {
10 	return true
11 }
12 
13-func createRouter(handler *prose.DbHandler) proxy.Router {
14+func createRouter(handler *filehandlers.ScpUploadHandler) proxy.Router {
15 	return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
16 		cmd := s.Command()
17 		mdw := []wish.Middleware{}
18@@ -45,7 +46,7 @@ func createRouter(handler *prose.DbHandler) proxy.Router {
19 	}
20 }
21 
22-func withProxy(handler *prose.DbHandler) ssh.Option {
23+func withProxy(handler *filehandlers.ScpUploadHandler) ssh.Option {
24 	return func(server *ssh.Server) error {
25 		err := sftp.SSHOption(handler)(server)
26 		if err != nil {
27@@ -63,7 +64,10 @@ func main() {
28 	logger := cfg.Logger
29 	dbh := postgres.NewDB(&cfg.ConfigCms)
30 	defer dbh.Close()
31-	handler := prose.NewDbHandler(dbh, cfg)
32+	fileHandler := &prose.ProseHandler{
33+		Cfg: cfg,
34+	}
35+	handler := filehandlers.NewScpPostHandler(dbh, cfg, fileHandler)
36 
37 	sshServer := &SSHServer{}
38 	s, err := wish.NewServer(
R pastes/db_handler.go => filehandlers/post_handler.go
+62, -35
  1@@ -1,9 +1,8 @@
  2-package pastes
  3+package filehandlers
  4 
  5 import (
  6 	"fmt"
  7 	"io"
  8-	"math"
  9 	"time"
 10 
 11 	"git.sr.ht/~erock/pico/shared"
 12@@ -13,37 +12,36 @@ import (
 13 	"github.com/gliderlabs/ssh"
 14 )
 15 
 16-// IsTextFile reports whether the file has a known extension indicating
 17-// a text file, or if a significant chunk of the specified file looks like
 18-// correct UTF-8; that is, if it is likely that the file contains human-
 19-// readable text.
 20-func IsTextFile(text string, filename string) bool {
 21-	num := math.Min(float64(len(text)), 1024)
 22-	return shared.IsText(text[0:int(num)])
 23+type PostMetaData struct {
 24+	Text        string
 25+	Title       string
 26+	Description string
 27+	PublishAt   *time.Time
 28+	Hidden      bool
 29+	Filename    string
 30 }
 31 
 32-type Opener struct {
 33-	entry *utils.FileEntry
 34+type ScpFileHooks interface {
 35+	FileValidate(text string, filename string) (bool, error)
 36+	FileMeta(text string, data *PostMetaData) error
 37 }
 38 
 39-func (o *Opener) Open(name string) (io.Reader, error) {
 40-	return o.entry.Reader, nil
 41-}
 42-
 43-type DbHandler struct {
 44+type ScpUploadHandler struct {
 45 	User   *db.User
 46 	DBPool db.DB
 47 	Cfg    *shared.ConfigSite
 48+	Hooks  ScpFileHooks
 49 }
 50 
 51-func NewDbHandler(dbpool db.DB, cfg *shared.ConfigSite) *DbHandler {
 52-	return &DbHandler{
 53+func NewScpPostHandler(dbpool db.DB, cfg *shared.ConfigSite, hooks ScpFileHooks) *ScpUploadHandler {
 54+	return &ScpUploadHandler{
 55 		DBPool: dbpool,
 56 		Cfg:    cfg,
 57+		Hooks:  hooks,
 58 	}
 59 }
 60 
 61-func (h *DbHandler) Validate(s ssh.Session) error {
 62+func (h *ScpUploadHandler) Validate(s ssh.Session) error {
 63 	var err error
 64 	key, err := util.KeyText(s)
 65 	if err != nil {
 66@@ -63,16 +61,10 @@ func (h *DbHandler) Validate(s ssh.Session) error {
 67 	return nil
 68 }
 69 
 70-func (h *DbHandler) Write(s ssh.Session, entry *utils.FileEntry) (string, error) {
 71+func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string, error) {
 72 	logger := h.Cfg.Logger
 73 	userID := h.User.ID
 74 	filename := entry.Name
 75-	title := filename
 76-	var err error
 77-	post, err := h.DBPool.FindPostWithFilename(filename, userID, h.Cfg.Space)
 78-	if err != nil {
 79-		logger.Debug("unable to load post, continuing:", err)
 80-	}
 81 
 82 	user, err := h.DBPool.FindUser(userID)
 83 	if err != nil {
 84@@ -84,9 +76,31 @@ func (h *DbHandler) Write(s ssh.Session, entry *utils.FileEntry) (string, error)
 85 		text = string(b)
 86 	}
 87 
 88-	if !IsTextFile(text, entry.Filepath) {
 89-		logger.Errorf("WARNING: (%s) invalid file, the contents must be plain text, skipping", entry.Name)
 90-		return "", fmt.Errorf("WARNING: (%s) invalid file, the contents must be plain text, skipping", entry.Name)
 91+	valid, err := h.Hooks.FileValidate(text, entry.Filepath)
 92+	if !valid {
 93+		return "", err
 94+	}
 95+
 96+	post, err := h.DBPool.FindPostWithFilename(filename, userID, h.Cfg.Space)
 97+	if err != nil {
 98+		logger.Debugf("unable to load post (%s), continuing", filename)
 99+		logger.Debug(err)
100+	}
101+
102+	now := time.Now()
103+	metadata := PostMetaData{
104+		Filename:  filename,
105+		Title:     shared.SanitizeFileExt(filename),
106+		PublishAt: &now,
107+	}
108+	if post != nil {
109+		metadata.PublishAt = post.PublishAt
110+	}
111+
112+	err = h.Hooks.FileMeta(text, &metadata)
113+	if err != nil {
114+		logger.Error(err)
115+		return "", err
116 	}
117 
118 	// if the file is empty we remove it from our database
119@@ -104,27 +118,40 @@ func (h *DbHandler) Write(s ssh.Session, entry *utils.FileEntry) (string, error)
120 			return "", fmt.Errorf("error for %s: %v", filename, err)
121 		}
122 	} else if post == nil {
123-		publishAt := time.Now()
124 		logger.Infof("(%s) not found, adding record", filename)
125-		_, err = h.DBPool.InsertPost(userID, filename, title, text, "", &publishAt, false, h.Cfg.Space)
126+		_, err = h.DBPool.InsertPost(
127+			userID,
128+			filename,
129+			metadata.Title,
130+			text,
131+			metadata.Description,
132+			metadata.PublishAt,
133+			metadata.Hidden,
134+			h.Cfg.Space,
135+		)
136 		if err != nil {
137 			logger.Errorf("error for %s: %v", filename, err)
138 			return "", fmt.Errorf("error for %s: %v", filename, err)
139 		}
140 	} else {
141-		publishAt := post.PublishAt
142 		if text == post.Text {
143 			logger.Infof("(%s) found, but text is identical, skipping", filename)
144-			return h.Cfg.PostURL(user.Name, filename), nil
145+			return h.Cfg.FullPostURL(user.Name, filename, h.Cfg.IsSubdomains(), true), nil
146 		}
147 
148 		logger.Infof("(%s) found, updating record", filename)
149-		_, err = h.DBPool.UpdatePost(post.ID, title, text, "", publishAt)
150+		_, err = h.DBPool.UpdatePost(
151+			post.ID,
152+			metadata.Title,
153+			text,
154+			metadata.Description,
155+			metadata.PublishAt,
156+		)
157 		if err != nil {
158 			logger.Errorf("error for %s: %v", filename, err)
159 			return "", fmt.Errorf("error for %s: %v", filename, err)
160 		}
161 	}
162 
163-	return h.Cfg.PostURL(user.Name, filename), nil
164+	return h.Cfg.FullPostURL(user.Name, filename, h.Cfg.IsSubdomains(), true), nil
165 }
M lists/api.go
+1, -1
1@@ -505,7 +505,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
2 
3 	var feedItems []*feeds.Item
4 	for _, post := range posts {
5-		if slices.Contains(HiddenPosts, post.Filename) {
6+		if slices.Contains(cfg.HiddenPosts, post.Filename) {
7 			continue
8 		}
9 
M lists/config.go
+2, -0
1@@ -36,6 +36,8 @@ func NewConfigSite() *shared.ConfigSite {
2 			Description: "A microblog for your lists.",
3 			IntroText:   intro,
4 			Space:       "lists",
5+			AllowedExt:  []string{".txt"},
6+			HiddenPosts: []string{"_header.txt", "_readme.txt"},
7 			Logger:      shared.CreateLogger(),
8 		},
9 	}
D lists/db_handler.go
+0, -135
  1@@ -1,135 +0,0 @@
  2-package lists
  3-
  4-import (
  5-	"fmt"
  6-	"io"
  7-	"time"
  8-
  9-	"git.sr.ht/~erock/pico/lists/pkg"
 10-	"git.sr.ht/~erock/pico/shared"
 11-	"git.sr.ht/~erock/pico/wish/cms/db"
 12-	"git.sr.ht/~erock/pico/wish/cms/util"
 13-	sendutils "git.sr.ht/~erock/pico/wish/send/utils"
 14-	"github.com/gliderlabs/ssh"
 15-	"golang.org/x/exp/slices"
 16-)
 17-
 18-var HiddenPosts = []string{"_readme", "_header"}
 19-var allowedExtensions = []string{".txt"}
 20-
 21-type Opener struct {
 22-	entry *sendutils.FileEntry
 23-}
 24-
 25-func (o *Opener) Open(name string) (io.Reader, error) {
 26-	return o.entry.Reader, nil
 27-}
 28-
 29-type DbHandler struct {
 30-	User   *db.User
 31-	DBPool db.DB
 32-	Cfg    *shared.ConfigSite
 33-}
 34-
 35-func NewDbHandler(dbpool db.DB, cfg *shared.ConfigSite) *DbHandler {
 36-	return &DbHandler{
 37-		DBPool: dbpool,
 38-		Cfg:    cfg,
 39-	}
 40-}
 41-
 42-func (h *DbHandler) Validate(s ssh.Session) error {
 43-	var err error
 44-	key, err := util.KeyText(s)
 45-	if err != nil {
 46-		return fmt.Errorf("key not found")
 47-	}
 48-
 49-	user, err := h.DBPool.FindUserForKey(s.User(), key)
 50-	if err != nil {
 51-		return err
 52-	}
 53-
 54-	if user.Name == "" {
 55-		return fmt.Errorf("must have username set")
 56-	}
 57-
 58-	h.User = user
 59-	return nil
 60-}
 61-
 62-func (h *DbHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (string, error) {
 63-	logger := h.Cfg.Logger
 64-	userID := h.User.ID
 65-	filename := shared.SanitizeFileExt(entry.Name)
 66-	title := filename
 67-
 68-	post, err := h.DBPool.FindPostWithFilename(filename, userID, h.Cfg.Space)
 69-	if err != nil {
 70-		logger.Debug("unable to load post, continuing:", err)
 71-	}
 72-
 73-	user, err := h.DBPool.FindUser(userID)
 74-	if err != nil {
 75-		return "", fmt.Errorf("error for %s: %v", filename, err)
 76-	}
 77-
 78-	var text string
 79-	if b, err := io.ReadAll(entry.Reader); err == nil {
 80-		text = string(b)
 81-	}
 82-
 83-	if !shared.IsTextFile(text, entry.Filepath, allowedExtensions) {
 84-		return "", fmt.Errorf("WARNING: (%s) invalid file, format must be '.txt' and the contents must be plain text, skipping", entry.Name)
 85-	}
 86-
 87-	parsedText := pkg.ParseText(text)
 88-	if parsedText.MetaData.Title != "" {
 89-		title = parsedText.MetaData.Title
 90-	}
 91-	description := parsedText.MetaData.Description
 92-
 93-	// if the file is empty we remove it from our database
 94-	if len(text) == 0 {
 95-		// skip empty files from being added to db
 96-		if post == nil {
 97-			logger.Infof("(%s) is empty, skipping record", filename)
 98-			return "", nil
 99-		}
100-
101-		err := h.DBPool.RemovePosts([]string{post.ID})
102-		logger.Infof("(%s) is empty, removing record", filename)
103-		if err != nil {
104-			return "", fmt.Errorf("error for %s: %v", filename, err)
105-		}
106-	} else if post == nil {
107-		publishAt := time.Now()
108-		if parsedText.MetaData.PublishAt != nil {
109-			publishAt = *parsedText.MetaData.PublishAt
110-		}
111-		hidden := slices.Contains(HiddenPosts, filename)
112-
113-		logger.Infof("(%s) not found, adding record", filename)
114-		_, err = h.DBPool.InsertPost(userID, filename, title, text, description, &publishAt, hidden, h.Cfg.Space)
115-		if err != nil {
116-			return "", fmt.Errorf("error for %s: %v", filename, err)
117-		}
118-	} else {
119-		publishAt := post.PublishAt
120-		if parsedText.MetaData.PublishAt != nil {
121-			publishAt = parsedText.MetaData.PublishAt
122-		}
123-		if text == post.Text {
124-			logger.Infof("(%s) found, but text is identical, skipping", filename)
125-			return h.Cfg.PostURL(user.Name, filename), nil
126-		}
127-
128-		logger.Infof("(%s) found, updating record", filename)
129-		_, err = h.DBPool.UpdatePost(post.ID, title, text, description, publishAt)
130-		if err != nil {
131-			return "", fmt.Errorf("error for %s: %v", filename, err)
132-		}
133-	}
134-
135-	return h.Cfg.PostURL(user.Name, filename), nil
136-}
M lists/gemini/gemini.go
+1, -1
1@@ -403,7 +403,7 @@ func rssBlogHandler(ctx context.Context, w gemini.ResponseWriter, r *gemini.Requ
2 
3 	var feedItems []*feeds.Item
4 	for _, post := range posts {
5-		if slices.Contains(lists.HiddenPosts, post.Filename) {
6+		if slices.Contains(cfg.HiddenPosts, post.Filename) {
7 			continue
8 		}
9 		parsed := pkg.ParseText(post.Text)
A lists/scp_handler.go
+47, -0
 1@@ -0,0 +1,47 @@
 2+package lists
 3+
 4+import (
 5+	"fmt"
 6+	"strings"
 7+
 8+	"git.sr.ht/~erock/pico/filehandlers"
 9+	"git.sr.ht/~erock/pico/lists/pkg"
10+	"git.sr.ht/~erock/pico/shared"
11+	"golang.org/x/exp/slices"
12+)
13+
14+type ListsHandler struct {
15+	Cfg *shared.ConfigSite
16+}
17+
18+func (p *ListsHandler) FileValidate(text string, filename string) (bool, error) {
19+	if !shared.IsTextFile(text, filename, p.Cfg.AllowedExt) {
20+		extStr := strings.Join(p.Cfg.AllowedExt, ",")
21+		err := fmt.Errorf(
22+			"WARNING: (%s) invalid file, format must be (%s) and the contents must be plain text, skipping",
23+			filename,
24+			extStr,
25+		)
26+		return false, err
27+	}
28+
29+	return true, nil
30+}
31+
32+func (p *ListsHandler) FileMeta(text string, data *filehandlers.PostMetaData) error {
33+	parsedText := pkg.ParseText(text)
34+
35+	if parsedText.MetaData.Title != "" {
36+		data.Title = parsedText.MetaData.Title
37+	}
38+
39+	data.Description = parsedText.MetaData.Description
40+
41+	if parsedText.MetaData.PublishAt != nil && !parsedText.MetaData.PublishAt.IsZero() {
42+		data.PublishAt = parsedText.MetaData.PublishAt
43+	}
44+
45+	data.Hidden = slices.Contains(p.Cfg.HiddenPosts, data.Filename)
46+
47+	return nil
48+}
A pastes/scp_handler.go
+28, -0
 1@@ -0,0 +1,28 @@
 2+package pastes
 3+
 4+import (
 5+	"fmt"
 6+
 7+	"git.sr.ht/~erock/pico/filehandlers"
 8+	"git.sr.ht/~erock/pico/shared"
 9+)
10+
11+type PastesHandler struct {
12+	Cfg *shared.ConfigSite
13+}
14+
15+func (p *PastesHandler) FileValidate(text string, filename string) (bool, error) {
16+	if !shared.IsTextFile(text, filename, p.Cfg.AllowedExt) {
17+		err := fmt.Errorf(
18+			"WARNING: (%s) invalid file, the contents must be plain text, skipping",
19+			filename,
20+		)
21+		return false, err
22+	}
23+
24+	return true, nil
25+}
26+
27+func (p *PastesHandler) FileMeta(text string, data *filehandlers.PostMetaData) error {
28+	return nil
29+}
M prose/api.go
+1, -1
1@@ -612,7 +612,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
2 
3 	var feedItems []*feeds.Item
4 	for _, post := range posts {
5-		if slices.Contains(hiddenPosts, post.Filename) {
6+		if slices.Contains(cfg.HiddenPosts, post.Filename) {
7 			continue
8 		}
9 		parsed, err := ParseText(post.Text)
M prose/config.go
+2, -0
1@@ -43,6 +43,8 @@ func NewConfigSite() *shared.ConfigSite {
2 			Description: "a blog platform for hackers.",
3 			IntroText:   intro,
4 			Space:       "prose",
5+			AllowedExt:  []string{".md", ".css"},
6+			HiddenPosts: []string{"_readme.md", "_styles.css"},
7 			Logger:      shared.CreateLogger(),
8 		},
9 	}
D prose/db_handler.go
+0, -145
  1@@ -1,145 +0,0 @@
  2-package prose
  3-
  4-import (
  5-	"fmt"
  6-	"io"
  7-	"strings"
  8-	"time"
  9-
 10-	"git.sr.ht/~erock/pico/shared"
 11-	"git.sr.ht/~erock/pico/wish/cms/db"
 12-	"git.sr.ht/~erock/pico/wish/cms/util"
 13-	"git.sr.ht/~erock/pico/wish/send/utils"
 14-	"github.com/gliderlabs/ssh"
 15-	"golang.org/x/exp/slices"
 16-)
 17-
 18-var hiddenPosts = []string{"_readme.md", "_styles.css"}
 19-var allowedExtensions = []string{".md", ".css"}
 20-
 21-type Opener struct {
 22-	entry *utils.FileEntry
 23-}
 24-
 25-func (o *Opener) Open(name string) (io.Reader, error) {
 26-	return o.entry.Reader, nil
 27-}
 28-
 29-type DbHandler struct {
 30-	User   *db.User
 31-	DBPool db.DB
 32-	Cfg    *shared.ConfigSite
 33-}
 34-
 35-func NewDbHandler(dbpool db.DB, cfg *shared.ConfigSite) *DbHandler {
 36-	return &DbHandler{
 37-		DBPool: dbpool,
 38-		Cfg:    cfg,
 39-	}
 40-}
 41-
 42-func (h *DbHandler) Validate(s ssh.Session) error {
 43-	var err error
 44-	key, err := util.KeyText(s)
 45-	if err != nil {
 46-		return fmt.Errorf("key not found")
 47-	}
 48-
 49-	user, err := h.DBPool.FindUserForKey(s.User(), key)
 50-	if err != nil {
 51-		return err
 52-	}
 53-
 54-	if user.Name == "" {
 55-		return fmt.Errorf("must have username set")
 56-	}
 57-
 58-	h.User = user
 59-	return nil
 60-}
 61-
 62-func (h *DbHandler) Write(s ssh.Session, entry *utils.FileEntry) (string, error) {
 63-	logger := h.Cfg.Logger
 64-	userID := h.User.ID
 65-	filename := shared.SanitizeFileExt(entry.Name)
 66-	title := filename
 67-	var err error
 68-	post, err := h.DBPool.FindPostWithFilename(filename, userID, h.Cfg.Space)
 69-	if err != nil {
 70-		logger.Debug("unable to load post, continuing:", err)
 71-	}
 72-
 73-	user, err := h.DBPool.FindUser(userID)
 74-	if err != nil {
 75-		return "", fmt.Errorf("error for %s: %v", filename, err)
 76-	}
 77-
 78-	var text string
 79-	if b, err := io.ReadAll(entry.Reader); err == nil {
 80-		text = string(b)
 81-	}
 82-
 83-	if !shared.IsTextFile(text, entry.Filepath, allowedExtensions) {
 84-		extStr := strings.Join(allowedExtensions, ",")
 85-		logger.Errorf("WARNING: (%s) invalid file, format must be (%s) and the contents must be plain text, skipping", entry.Name, extStr)
 86-		return "", fmt.Errorf("WARNING: (%s) invalid file, format must be (%s) and the contents must be plain text, skipping", entry.Name, extStr)
 87-	}
 88-
 89-	parsedText, err := ParseText(text)
 90-	if err != nil {
 91-		logger.Errorf("error for %s: %v", filename, err)
 92-		return "", fmt.Errorf("error for %s: %v", filename, err)
 93-	}
 94-
 95-	if parsedText.MetaData.Title != "" {
 96-		title = parsedText.MetaData.Title
 97-	}
 98-	description := parsedText.MetaData.Description
 99-
100-	// if the file is empty we remove it from our database
101-	if len(text) == 0 {
102-		// skip empty files from being added to db
103-		if post == nil {
104-			logger.Infof("(%s) is empty, skipping record", filename)
105-			return "", nil
106-		}
107-
108-		err := h.DBPool.RemovePosts([]string{post.ID})
109-		logger.Infof("(%s) is empty, removing record", filename)
110-		if err != nil {
111-			logger.Errorf("error for %s: %v", filename, err)
112-			return "", fmt.Errorf("error for %s: %v", filename, err)
113-		}
114-	} else if post == nil {
115-		publishAt := time.Now()
116-		if parsedText.MetaData.PublishAt != nil && !parsedText.MetaData.PublishAt.IsZero() {
117-			publishAt = *parsedText.MetaData.PublishAt
118-		}
119-		hidden := slices.Contains(hiddenPosts, entry.Name)
120-
121-		logger.Infof("(%s) not found, adding record", filename)
122-		_, err = h.DBPool.InsertPost(userID, filename, title, text, description, &publishAt, hidden, h.Cfg.Space)
123-		if err != nil {
124-			logger.Errorf("error for %s: %v", filename, err)
125-			return "", fmt.Errorf("error for %s: %v", filename, err)
126-		}
127-	} else {
128-		publishAt := post.PublishAt
129-		if parsedText.MetaData.PublishAt != nil {
130-			publishAt = parsedText.MetaData.PublishAt
131-		}
132-		if text == post.Text {
133-			logger.Infof("(%s) found, but text is identical, skipping", filename)
134-			return h.Cfg.FullPostURL(user.Name, filename, h.Cfg.IsSubdomains(), true), nil
135-		}
136-
137-		logger.Infof("(%s) found, updating record", filename)
138-		_, err = h.DBPool.UpdatePost(post.ID, title, text, description, publishAt)
139-		if err != nil {
140-			logger.Errorf("error for %s: %v", filename, err)
141-			return "", fmt.Errorf("error for %s: %v", filename, err)
142-		}
143-	}
144-
145-	return h.Cfg.FullPostURL(user.Name, filename, h.Cfg.IsSubdomains(), true), nil
146-}
A prose/scp_handler.go
+52, -0
 1@@ -0,0 +1,52 @@
 2+package prose
 3+
 4+import (
 5+	"fmt"
 6+	"strings"
 7+
 8+	"git.sr.ht/~erock/pico/filehandlers"
 9+	"git.sr.ht/~erock/pico/shared"
10+	"golang.org/x/exp/slices"
11+)
12+
13+// var hiddenPosts = []string{"_readme.md", "_styles.css"}
14+// var allowedExtensions = []string{".md", ".css"}
15+
16+type ProseHandler struct {
17+	Cfg *shared.ConfigSite
18+}
19+
20+func (p *ProseHandler) FileValidate(text string, filename string) (bool, error) {
21+	if !shared.IsTextFile(text, filename, p.Cfg.AllowedExt) {
22+		extStr := strings.Join(p.Cfg.AllowedExt, ",")
23+		err := fmt.Errorf(
24+			"WARNING: (%s) invalid file, format must be (%s) and the contents must be plain text, skipping",
25+			filename,
26+			extStr,
27+		)
28+		return false, err
29+	}
30+
31+	return true, nil
32+}
33+
34+func (p *ProseHandler) FileMeta(text string, data *filehandlers.PostMetaData) error {
35+	parsedText, err := ParseText(text)
36+	if err != nil {
37+		return err
38+	}
39+
40+	if parsedText.Title != "" {
41+		data.Title = parsedText.Title
42+	}
43+
44+	data.Description = parsedText.Description
45+
46+	if parsedText.PublishAt != nil && !parsedText.PublishAt.IsZero() {
47+		data.PublishAt = parsedText.MetaData.PublishAt
48+	}
49+
50+	data.Hidden = slices.Contains(p.Cfg.HiddenPosts, data.Filename)
51+
52+	return nil
53+}
M shared/util.go
+5, -3
 1@@ -75,9 +75,11 @@ func IsText(s string) bool {
 2 // correct UTF-8; that is, if it is likely that the file contains human-
 3 // readable text.
 4 func IsTextFile(text string, filename string, allowedExtensions []string) bool {
 5-	ext := pathpkg.Ext(filename)
 6-	if !slices.Contains(allowedExtensions, ext) {
 7-		return false
 8+	if len(allowedExtensions) > 0 {
 9+		ext := pathpkg.Ext(filename)
10+		if !slices.Contains(allowedExtensions, ext) {
11+			return false
12+		}
13 	}
14 
15 	num := math.Min(float64(len(text)), 1024)
M wish/cms/config/config.go
+2, -0
1@@ -18,6 +18,8 @@ type ConfigCms struct {
2 	Description string
3 	IntroText   string
4 	Space       string
5+	AllowedExt  []string
6+	HiddenPosts []string
7 	Logger      *zap.SugaredLogger
8 }
9