Commit e1a3a25

Antonio Mika  ·  2026-10-11 10:58:23 -0400 EDT
parent 61ecf7c
refactor(postfs): move post file ops out of filehandlers and the router into prose
12 files changed,  +345, -335
+12, -12
......@@ -11,7 +11,7 @@ import (
1111
1212 "github.com/adhocore/gronx"
1313 "github.com/picosh/pico/pkg/db"
14- "github.com/picosh/pico/pkg/filehandlers"
14+ "github.com/picosh/pico/pkg/postfs"
1515 "github.com/picosh/pico/pkg/pssh"
1616 sendutils "github.com/picosh/pico/pkg/send/utils"
1717 "github.com/picosh/pico/pkg/shared"
......@@ -38,7 +38,7 @@ func (h *UploadHandler) GetLogger(s *pssh.SSHServerConnSession) *slog.Logger {
3838 }
3939
4040 func (h *UploadHandler) Validate(s *pssh.SSHServerConnSession) error {
41- user, err := filehandlers.SessionUser(s)
41+ user, err := postfs.SessionUser(s)
4242 if err != nil {
4343 return err
4444 }
......@@ -47,36 +47,36 @@ func (h *UploadHandler) Validate(s *pssh.SSHServerConnSession) error {
4747 }
4848
4949 func (h *UploadHandler) List(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
50- return filehandlers.BaseList(s, fpath, isDir, recursive, []string{h.Cfg.Space}, h.DB)
50+ return postfs.ListPosts(s, fpath, isDir, recursive, []string{h.Cfg.Space}, h.DB)
5151 }
5252
5353 func (h *UploadHandler) Read(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReadAndReaderAtCloser, error) {
54- return filehandlers.ReadPost(s, h.DB, h.Cfg.Space, entry)
54+ return postfs.ReadPost(s, h.DB, h.Cfg.Space, entry)
5555 }
5656
5757 func (h *UploadHandler) Delete(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) error {
58- return filehandlers.DeletePost(s, h.DB, h.Cfg.Space, entry)
58+ return postfs.DeletePost(s, h.DB, h.Cfg.Space, entry)
5959 }
6060
6161 // ValidateWrite checks a feed's name before its contents arrive.
6262 func (h *UploadHandler) ValidateWrite(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) error {
63- if _, err := filehandlers.SessionUser(s); err != nil {
63+ if _, err := postfs.SessionUser(s); err != nil {
6464 return err
6565 }
6666 if entry.Mode.IsDir() {
6767 return fmt.Errorf("file entry is directory, but only files are supported: %s", entry.Filepath)
6868 }
69- return h.validate(filehandlers.NewPost(filepath.Base(entry.Filepath), nil))
69+ return h.validate(postfs.NewPost(filepath.Base(entry.Filepath), nil))
7070 }
7171
7272 func (h *UploadHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (string, error) {
73- user, err := filehandlers.SessionUser(s)
73+ user, err := postfs.SessionUser(s)
7474 if err != nil {
7575 return "", err
7676 }
7777 logger := h.GetLogger(s).With("filename", filepath.Base(entry.Filepath))
7878
79- post, err := filehandlers.PostFromEntry(entry, user, h.Cfg.Space)
79+ post, err := postfs.PostFromEntry(entry, user, h.Cfg.Space)
8080 if err != nil {
8181 return "", err
8282 }
......@@ -85,7 +85,7 @@ func (h *UploadHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.Fil
8585 return "", err
8686 }
8787
88- cur, err := filehandlers.LookupPost(h.DB, user.ID, h.Cfg.Space, post.Filename)
88+ cur, err := postfs.LookupPost(h.DB, user.ID, h.Cfg.Space, post.Filename)
8989 if err != nil {
9090 return "", err
9191 }
......@@ -94,7 +94,7 @@ func (h *UploadHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.Fil
9494 post.Data, post.PublishAt = cur.Data, cur.PublishAt
9595 }
9696
97- _, changed, err := filehandlers.SavePost(h.DB, cur, post)
97+ _, changed, err := postfs.SavePost(h.DB, cur, post)
9898 if err != nil {
9999 logger.Error("feed could not be saved", "err", err.Error())
100100 return "", err
......@@ -105,7 +105,7 @@ func (h *UploadHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.Fil
105105
106106 // Rename moves a feed to a new filename in place, keeping its digest state.
107107 func (h *UploadHandler) Rename(s *pssh.SSHServerConnSession, from, to *sendutils.FileEntry) error {
108- return filehandlers.RenamePost(s, h.DB, h.Cfg.Space, from, to, func(cur, next *db.Post) ([]string, error) {
108+ return postfs.RenamePost(s, h.DB, h.Cfg.Space, from, to, func(cur, next *db.Post) ([]string, error) {
109109 return nil, h.validate(next)
110110 })
111111 }
+12, -12
......@@ -11,7 +11,7 @@ import (
1111
1212 "github.com/araddon/dateparse"
1313 "github.com/picosh/pico/pkg/db"
14- "github.com/picosh/pico/pkg/filehandlers"
14+ "github.com/picosh/pico/pkg/postfs"
1515 "github.com/picosh/pico/pkg/pssh"
1616 sendutils "github.com/picosh/pico/pkg/send/utils"
1717 "github.com/picosh/pico/pkg/shared"
......@@ -47,7 +47,7 @@ func (h *UploadHandler) GetLogger(s *pssh.SSHServerConnSession) *slog.Logger {
4747 }
4848
4949 func (h *UploadHandler) Validate(s *pssh.SSHServerConnSession) error {
50- user, err := filehandlers.SessionUser(s)
50+ user, err := postfs.SessionUser(s)
5151 if err != nil {
5252 return err
5353 }
......@@ -59,36 +59,36 @@ func (h *UploadHandler) Validate(s *pssh.SSHServerConnSession) error {
5959 }
6060
6161 func (h *UploadHandler) List(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
62- return filehandlers.BaseList(s, fpath, isDir, recursive, []string{h.Cfg.Space}, h.DB)
62+ return postfs.ListPosts(s, fpath, isDir, recursive, []string{h.Cfg.Space}, h.DB)
6363 }
6464
6565 func (h *UploadHandler) Read(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReadAndReaderAtCloser, error) {
66- return filehandlers.ReadPost(s, h.DB, h.Cfg.Space, entry)
66+ return postfs.ReadPost(s, h.DB, h.Cfg.Space, entry)
6767 }
6868
6969 func (h *UploadHandler) Delete(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) error {
70- return filehandlers.DeletePost(s, h.DB, h.Cfg.Space, entry)
70+ return postfs.DeletePost(s, h.DB, h.Cfg.Space, entry)
7171 }
7272
7373 // ValidateWrite checks a paste's name before its contents arrive.
7474 func (h *UploadHandler) ValidateWrite(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) error {
75- if _, err := filehandlers.SessionUser(s); err != nil {
75+ if _, err := postfs.SessionUser(s); err != nil {
7676 return err
7777 }
7878 if entry.Mode.IsDir() {
7979 return fmt.Errorf("file entry is directory, but only files are supported: %s", entry.Filepath)
8080 }
81- return h.validate(filehandlers.NewPost(filepath.Base(entry.Filepath), nil))
81+ return h.validate(postfs.NewPost(filepath.Base(entry.Filepath), nil))
8282 }
8383
8484 func (h *UploadHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (string, error) {
85- user, err := filehandlers.SessionUser(s)
85+ user, err := postfs.SessionUser(s)
8686 if err != nil {
8787 return "", err
8888 }
8989 logger := h.GetLogger(s).With("filename", filepath.Base(entry.Filepath))
9090
91- post, err := filehandlers.PostFromEntry(entry, user, h.Cfg.Space)
91+ post, err := postfs.PostFromEntry(entry, user, h.Cfg.Space)
9292 if err != nil {
9393 return "", err
9494 }
......@@ -97,7 +97,7 @@ func (h *UploadHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.Fil
9797 return "", err
9898 }
9999
100- cur, err := filehandlers.LookupPost(h.DB, user.ID, h.Cfg.Space, post.Filename)
100+ cur, err := postfs.LookupPost(h.DB, user.ID, h.Cfg.Space, post.Filename)
101101 if err != nil {
102102 return "", err
103103 }
......@@ -106,7 +106,7 @@ func (h *UploadHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.Fil
106106 }
107107 setMeta(s, cur, post)
108108
109- _, changed, err := filehandlers.SavePost(h.DB, cur, post)
109+ _, changed, err := postfs.SavePost(h.DB, cur, post)
110110 if err != nil {
111111 logger.Error("paste could not be saved", "err", err.Error())
112112 return "", err
......@@ -117,7 +117,7 @@ func (h *UploadHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.Fil
117117
118118 // Rename moves a paste to a new filename in place, keeping its settings.
119119 func (h *UploadHandler) Rename(s *pssh.SSHServerConnSession, from, to *sendutils.FileEntry) error {
120- return filehandlers.RenamePost(s, h.DB, h.Cfg.Space, from, to, func(cur, next *db.Post) ([]string, error) {
120+ return postfs.RenamePost(s, h.DB, h.Cfg.Space, from, to, func(cur, next *db.Post) ([]string, error) {
121121 if err := h.validate(next); err != nil {
122122 return nil, err
123123 }
+2, -2
......@@ -9,7 +9,7 @@ import (
99
1010 "github.com/picosh/pico/pkg/db"
1111 "github.com/picosh/pico/pkg/db/stub"
12- "github.com/picosh/pico/pkg/filehandlers"
12+ "github.com/picosh/pico/pkg/postfs"
1313 "github.com/picosh/pico/pkg/shared/router"
1414 "github.com/picosh/pico/pkg/storage"
1515 )
......@@ -23,7 +23,7 @@ func TestRoutes(t *testing.T) {
2323 cfg := NewConfig("prose-test")
2424 cfg.Logger = logger
2525 for _, name := range []string{"hello", "raw"} {
26- post := filehandlers.NewPost(name+".md", []byte("# "+name))
26+ post := postfs.NewPost(name+".md", []byte("# "+name))
2727 post.UserID, post.Space, post.Title = user.ID, cfg.Space, name
2828 if _, err := posts.InsertPost(post); err != nil {
2929 t.Fatal(err)
R pkg/apps/prose/posts.go => pkg/apps/prose/post_uploader.go
+14, -14
......@@ -8,7 +8,7 @@ import (
88 "strings"
99
1010 "github.com/picosh/pico/pkg/db"
11- "github.com/picosh/pico/pkg/filehandlers"
11+ "github.com/picosh/pico/pkg/postfs"
1212 "github.com/picosh/pico/pkg/pssh"
1313 sendutils "github.com/picosh/pico/pkg/send/utils"
1414 "github.com/picosh/pico/pkg/shared"
......@@ -21,42 +21,42 @@ type postUploader struct {
2121 }
2222
2323 var (
24- _ filehandlers.ReadWriteHandler = (*postUploader)(nil)
25- _ sendutils.WriteValidator = (*postUploader)(nil)
26- _ sendutils.FileRenamer = (*postUploader)(nil)
24+ _ ReadWriteHandler = (*postUploader)(nil)
25+ _ sendutils.WriteValidator = (*postUploader)(nil)
26+ _ sendutils.FileRenamer = (*postUploader)(nil)
2727 )
2828
2929 func (h *postUploader) List(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
30- return filehandlers.BaseList(s, fpath, isDir, recursive, []string{h.Cfg.Space}, h.DB)
30+ return postfs.ListPosts(s, fpath, isDir, recursive, []string{h.Cfg.Space}, h.DB)
3131 }
3232
3333 func (h *postUploader) Read(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReadAndReaderAtCloser, error) {
34- return filehandlers.ReadPost(s, h.DB, h.Cfg.Space, entry)
34+ return postfs.ReadPost(s, h.DB, h.Cfg.Space, entry)
3535 }
3636
3737 func (h *postUploader) Delete(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) error {
38- return filehandlers.DeletePost(s, h.DB, h.Cfg.Space, entry)
38+ return postfs.DeletePost(s, h.DB, h.Cfg.Space, entry)
3939 }
4040
4141 // ValidateWrite checks a post's name before its contents arrive.
4242 func (h *postUploader) ValidateWrite(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) error {
43- if _, err := filehandlers.SessionUser(s); err != nil {
43+ if _, err := postfs.SessionUser(s); err != nil {
4444 return err
4545 }
4646 if entry.Mode.IsDir() {
4747 return fmt.Errorf("file entry is directory, but only files are supported: %s", entry.Filepath)
4848 }
49- return h.validate(filehandlers.NewPost(filepath.Base(entry.Filepath), nil))
49+ return h.validate(postfs.NewPost(filepath.Base(entry.Filepath), nil))
5050 }
5151
5252 func (h *postUploader) Write(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (string, error) {
53- user, err := filehandlers.SessionUser(s)
53+ user, err := postfs.SessionUser(s)
5454 if err != nil {
5555 return "", err
5656 }
5757 logger := pssh.GetLogger(s).With("filename", filepath.Base(entry.Filepath))
5858
59- post, err := filehandlers.PostFromEntry(entry, user, h.Cfg.Space)
59+ post, err := postfs.PostFromEntry(entry, user, h.Cfg.Space)
6060 if err != nil {
6161 return "", err
6262 }
......@@ -65,7 +65,7 @@ func (h *postUploader) Write(s *pssh.SSHServerConnSession, entry *sendutils.File
6565 return "", err
6666 }
6767
68- cur, err := filehandlers.LookupPost(h.DB, user.ID, h.Cfg.Space, post.Filename)
68+ cur, err := postfs.LookupPost(h.DB, user.ID, h.Cfg.Space, post.Filename)
6969 if err != nil {
7070 return "", err
7171 }
......@@ -78,7 +78,7 @@ func (h *postUploader) Write(s *pssh.SSHServerConnSession, entry *sendutils.File
7878 return "", err
7979 }
8080
81- saved, changed, err := filehandlers.SavePost(h.DB, cur, post)
81+ saved, changed, err := postfs.SavePost(h.DB, cur, post)
8282 if err != nil {
8383 logger.Error("post could not be saved", "err", err.Error())
8484 return "", err
......@@ -104,7 +104,7 @@ func (h *postUploader) Write(s *pssh.SSHServerConnSession, entry *sendutils.File
104104 // Rename moves a post to a new filename in place, so it keeps its ID,
105105 // publish date and settings. A post at the new filename is replaced.
106106 func (h *postUploader) Rename(s *pssh.SSHServerConnSession, from, to *sendutils.FileEntry) error {
107- return filehandlers.RenamePost(s, h.DB, h.Cfg.Space, from, to, func(cur, next *db.Post) ([]string, error) {
107+ return postfs.RenamePost(s, h.DB, h.Cfg.Space, from, to, func(cur, next *db.Post) ([]string, error) {
108108 if err := h.validate(next); err != nil {
109109 return nil, err
110110 }
+154, -0
......@@ -0,0 +1,154 @@
1+package prose
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "log/slog"
7+ "os"
8+ "path/filepath"
9+
10+ "github.com/picosh/pico/pkg/pssh"
11+ sendutils "github.com/picosh/pico/pkg/send/utils"
12+)
13+
14+// ReadWriteHandler serves the files with the extensions it is routed.
15+type ReadWriteHandler interface {
16+ List(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error)
17+ Write(*pssh.SSHServerConnSession, *sendutils.FileEntry) (string, error)
18+ Read(*pssh.SSHServerConnSession, *sendutils.FileEntry) (os.FileInfo, sendutils.ReadAndReaderAtCloser, error)
19+ Delete(*pssh.SSHServerConnSession, *sendutils.FileEntry) error
20+}
21+
22+// fileRouter sends each file to the handler for its extension.
23+type fileRouter struct {
24+ FileMap map[string]ReadWriteHandler
25+ Space string
26+}
27+
28+var _ sendutils.CopyFromClientHandler = (*fileRouter)(nil)
29+
30+func newFileRouter(space string, mapper map[string]ReadWriteHandler) *fileRouter {
31+ return &fileRouter{
32+ Space: space,
33+ FileMap: mapper,
34+ }
35+}
36+
37+func (r *fileRouter) findHandler(fp string) (ReadWriteHandler, error) {
38+ fext := filepath.Ext(fp)
39+ handler, ok := r.FileMap[fext]
40+ if !ok {
41+ hand, hasFallback := r.FileMap["fallback"]
42+ if !hasFallback {
43+ return nil, fmt.Errorf("no corresponding handler for file extension: %s", fext)
44+ }
45+ handler = hand
46+ }
47+ return handler, nil
48+}
49+
50+func (r *fileRouter) Write(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (string, error) {
51+ if entry.Mode.IsDir() {
52+ return "", os.ErrInvalid
53+ }
54+
55+ handler, err := r.findHandler(entry.Filepath)
56+ if err != nil {
57+ return "", err
58+ }
59+ return handler.Write(s, entry)
60+}
61+
62+func (r *fileRouter) ValidateWrite(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) error {
63+ handler, err := r.findHandler(entry.Filepath)
64+ if err != nil {
65+ return err
66+ }
67+ if v, ok := handler.(sendutils.WriteValidator); ok {
68+ return v.ValidateWrite(s, entry)
69+ }
70+ return nil
71+}
72+
73+func (r *fileRouter) Delete(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) error {
74+ handler, err := r.findHandler(entry.Filepath)
75+ if err != nil {
76+ return err
77+ }
78+ return handler.Delete(s, entry)
79+}
80+
81+// Rename moves a file in place when one handler serves both names.
82+func (r *fileRouter) Rename(s *pssh.SSHServerConnSession, from, to *sendutils.FileEntry) error {
83+ handler, err := r.findHandler(from.Filepath)
84+ if err != nil {
85+ return err
86+ }
87+ target, err := r.findHandler(to.Filepath)
88+ if err != nil {
89+ return err
90+ }
91+ renamer, ok := handler.(sendutils.FileRenamer)
92+ if !ok || handler != target {
93+ return errors.ErrUnsupported
94+ }
95+ return renamer.Rename(s, from, to)
96+}
97+
98+func (r *fileRouter) Read(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReadAndReaderAtCloser, error) {
99+ handler, err := r.findHandler(entry.Filepath)
100+ if err != nil {
101+ return nil, nil, err
102+ }
103+ return handler.Read(s, entry)
104+}
105+
106+func (r *fileRouter) List(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
107+ files := []os.FileInfo{}
108+ // Several extensions can share a handler, and handlers can list the
109+ // same entries, such as the root directory.
110+ listed := map[ReadWriteHandler]bool{}
111+ seen := map[string]bool{}
112+ for _, handler := range r.FileMap {
113+ if listed[handler] {
114+ continue
115+ }
116+ listed[handler] = true
117+
118+ ff, err := handler.List(s, fpath, isDir, recursive)
119+ if err != nil {
120+ r.GetLogger(s).Error("handler list", "err", err)
121+ continue
122+ }
123+ for _, f := range ff {
124+ key := fmt.Sprintf("%s:%v", f.Name(), f.IsDir())
125+ if !seen[key] {
126+ seen[key] = true
127+ files = append(files, f)
128+ }
129+ }
130+ }
131+ return files, nil
132+}
133+
134+func (r *fileRouter) GetLogger(s *pssh.SSHServerConnSession) *slog.Logger {
135+ return pssh.GetLogger(s)
136+}
137+
138+func (r *fileRouter) Validate(s *pssh.SSHServerConnSession) error {
139+ logger := pssh.GetLogger(s)
140+ user := pssh.GetUser(s)
141+
142+ if user == nil {
143+ err := fmt.Errorf("could not get user from ctx")
144+ logger.Error("error getting user from ctx", "err", err)
145+ return err
146+ }
147+
148+ logger.Info(
149+ "attempting to upload files",
150+ "user", user.Name,
151+ "space", r.Space,
152+ )
153+ return nil
154+}
+65, -0
......@@ -0,0 +1,65 @@
1+package prose
2+
3+import (
4+ "errors"
5+ "os"
6+ "testing"
7+
8+ "github.com/picosh/pico/pkg/pssh"
9+ sendutils "github.com/picosh/pico/pkg/send/utils"
10+)
11+
12+// listHandler lists names and records the renames it is sent.
13+type listHandler struct {
14+ ReadWriteHandler
15+ names []string
16+ renamed int
17+}
18+
19+func (h *listHandler) List(*pssh.SSHServerConnSession, string, bool, bool) ([]os.FileInfo, error) {
20+ files := []os.FileInfo{&sendutils.VirtualFile{FName: "/", FIsDir: true}}
21+ for _, name := range h.names {
22+ files = append(files, &sendutils.VirtualFile{FName: name})
23+ }
24+ return files, nil
25+}
26+
27+func (h *listHandler) Rename(*pssh.SSHServerConnSession, *sendutils.FileEntry, *sendutils.FileEntry) error {
28+ h.renamed++
29+ return nil
30+}
31+
32+func entry(name string) *sendutils.FileEntry {
33+ return &sendutils.FileEntry{Filepath: "/" + name}
34+}
35+
36+func TestRouterSharedHandler(t *testing.T) {
37+ posts := &listHandler{names: []string{"a.md"}}
38+ router := newFileRouter("prose", map[string]ReadWriteHandler{
39+ ".md": posts,
40+ ".lxt": posts,
41+ ".txt": &listHandler{names: []string{"a.md"}},
42+ "fallback": &listHandler{names: []string{"x.png"}},
43+ })
44+
45+ list, err := router.List(nil, "/", true, false)
46+ if err != nil {
47+ t.Fatal(err)
48+ }
49+ var names []string
50+ for _, f := range list {
51+ names = append(names, f.Name())
52+ }
53+ if len(names) != 3 {
54+ t.Errorf("listed %q, want /, a.md and x.png once each", names)
55+ }
56+
57+ if err := router.Rename(nil, entry("a.md"), entry("a.lxt")); err != nil || posts.renamed != 1 {
58+ t.Fatalf("rename within a handler: %v, renamed %d", err, posts.renamed)
59+ }
60+ for _, to := range []string{"a.txt", "a.png"} {
61+ if err := router.Rename(nil, entry("a.lxt"), entry(to)); !errors.Is(err, errors.ErrUnsupported) {
62+ t.Errorf("rename to %s across handlers: %v", to, err)
63+ }
64+ }
65+}
+8, -9
......@@ -5,7 +5,6 @@ import (
55 "log/slog"
66
77 "github.com/picosh/pico/pkg/db"
8- "github.com/picosh/pico/pkg/filehandlers"
98 "github.com/picosh/pico/pkg/pssh"
109 sendutils "github.com/picosh/pico/pkg/send/utils"
1110 "github.com/picosh/pico/pkg/storage"
......@@ -27,7 +26,7 @@ func setFeatureFlag(s *pssh.SSHServerConnSession, ff *db.FeatureFlag) {
2726 }
2827
2928 type UploadHandler struct {
30- *filehandlers.FileHandlerRouter
29+ *fileRouter
3130 Cfg *Config
3231 DB DB
3332 }
......@@ -35,12 +34,12 @@ type UploadHandler struct {
3534 var _ sendutils.CopyFromClientHandler = &UploadHandler{}
3635 var _ sendutils.CopyFromClientHandler = (*UploadHandler)(nil)
3736
38-func NewUploadHandler(cfg *Config, dbpool DB, fileMap map[string]filehandlers.ReadWriteHandler) *UploadHandler {
39- router := filehandlers.NewFileHandlerRouter(cfg.Space, fileMap)
37+func NewUploadHandler(cfg *Config, dbpool DB, fileMap map[string]ReadWriteHandler) *UploadHandler {
38+ router := newFileRouter(cfg.Space, fileMap)
4039 return &UploadHandler{
41- FileHandlerRouter: router,
42- Cfg: cfg,
43- DB: dbpool,
40+ fileRouter: router,
41+ Cfg: cfg,
42+ DB: dbpool,
4443 }
4544 }
4645
......@@ -49,7 +48,7 @@ func NewFileHandler(cfg *Config, dbpool DB, st storage.StorageServe) *UploadHand
4948 // One post handler serves every text extension, so a post can be
5049 // renamed in place from one to another.
5150 posts := &postUploader{Cfg: cfg, DB: dbpool}
52- return NewUploadHandler(cfg, dbpool, map[string]filehandlers.ReadWriteHandler{
51+ return NewUploadHandler(cfg, dbpool, map[string]ReadWriteHandler{
5352 ".md": posts,
5453 ".txt": posts,
5554 ".css": posts,
......@@ -86,5 +85,5 @@ func (h *UploadHandler) Validate(s *pssh.SSHServerConnSession) error {
8685 ff.SetLimits(h.Cfg.MaxSize, h.Cfg.MaxAssetSize, h.Cfg.MaxSpecialFileSize)
8786 setFeatureFlag(s, ff)
8887
89- return h.FileHandlerRouter.Validate(s)
88+ return h.fileRouter.Validate(s)
9089 }
+3, -4
......@@ -11,7 +11,6 @@ import (
1111
1212 "github.com/picosh/pico/pkg/db"
1313 "github.com/picosh/pico/pkg/db/stub"
14- "github.com/picosh/pico/pkg/filehandlers"
1514 "github.com/picosh/pico/pkg/pssh"
1615 sendutils "github.com/picosh/pico/pkg/send/utils"
1716 "github.com/picosh/pico/pkg/shared"
......@@ -69,7 +68,7 @@ func TestUploadHandlerValidate(t *testing.T) {
6968
7069 t.Run("nil user in session fails validation", func(t *testing.T) {
7170 mockDB := newMockFeatureDB()
72- handler := NewUploadHandler(cfg, mockDB, map[string]filehandlers.ReadWriteHandler{})
71+ handler := NewUploadHandler(cfg, mockDB, map[string]ReadWriteHandler{})
7372
7473 sesh := createSession(nil)
7574 err := handler.Validate(sesh)
......@@ -80,7 +79,7 @@ func TestUploadHandlerValidate(t *testing.T) {
8079
8180 t.Run("user without feature flag fails validation", func(t *testing.T) {
8281 mockDB := newMockFeatureDB()
83- handler := NewUploadHandler(cfg, mockDB, map[string]filehandlers.ReadWriteHandler{})
82+ handler := NewUploadHandler(cfg, mockDB, map[string]ReadWriteHandler{})
8483
8584 user := &db.User{ID: userID, Name: "tester"}
8685 sesh := createSession(user)
......@@ -96,7 +95,7 @@ func TestUploadHandlerValidate(t *testing.T) {
9695 proseFF.ExpiresAt = &validExpires
9796 mockDB.setFeature(userID, proseFF)
9897
99- handler := NewUploadHandler(cfg, mockDB, map[string]filehandlers.ReadWriteHandler{})
98+ handler := NewUploadHandler(cfg, mockDB, map[string]ReadWriteHandler{})
10099
101100 user := &db.User{ID: userID, Name: "tester"}
102101 sesh := createSession(user)
+0, -225
......@@ -1,225 +0,0 @@
1-package filehandlers
2-
3-import (
4- "database/sql"
5- "errors"
6- "fmt"
7- "log/slog"
8- "os"
9- "path/filepath"
10-
11- "github.com/picosh/pico/pkg/db"
12- "github.com/picosh/pico/pkg/pssh"
13- "github.com/picosh/pico/pkg/send/utils"
14-)
15-
16-type ReadWriteHandler interface {
17- List(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error)
18- Write(*pssh.SSHServerConnSession, *utils.FileEntry) (string, error)
19- Read(*pssh.SSHServerConnSession, *utils.FileEntry) (os.FileInfo, utils.ReadAndReaderAtCloser, error)
20- Delete(*pssh.SSHServerConnSession, *utils.FileEntry) error
21-}
22-
23-type FileHandlerRouter struct {
24- FileMap map[string]ReadWriteHandler
25- Space string
26-}
27-
28-var _ utils.CopyFromClientHandler = &FileHandlerRouter{} // Verify implementation
29-var _ utils.CopyFromClientHandler = (*FileHandlerRouter)(nil) // Verify implementation
30-
31-func NewFileHandlerRouter(space string, mapper map[string]ReadWriteHandler) *FileHandlerRouter {
32- return &FileHandlerRouter{
33- Space: space,
34- FileMap: mapper,
35- }
36-}
37-
38-func (r *FileHandlerRouter) findHandler(fp string) (ReadWriteHandler, error) {
39- fext := filepath.Ext(fp)
40- handler, ok := r.FileMap[fext]
41- if !ok {
42- hand, hasFallback := r.FileMap["fallback"]
43- if !hasFallback {
44- return nil, fmt.Errorf("no corresponding handler for file extension: %s", fext)
45- }
46- handler = hand
47- }
48- return handler, nil
49-}
50-
51-func (r *FileHandlerRouter) Write(s *pssh.SSHServerConnSession, entry *utils.FileEntry) (string, error) {
52- if entry.Mode.IsDir() {
53- return "", os.ErrInvalid
54- }
55-
56- handler, err := r.findHandler(entry.Filepath)
57- if err != nil {
58- return "", err
59- }
60- return handler.Write(s, entry)
61-}
62-
63-func (r *FileHandlerRouter) ValidateWrite(s *pssh.SSHServerConnSession, entry *utils.FileEntry) error {
64- handler, err := r.findHandler(entry.Filepath)
65- if err != nil {
66- return err
67- }
68- if v, ok := handler.(utils.WriteValidator); ok {
69- return v.ValidateWrite(s, entry)
70- }
71- return nil
72-}
73-
74-func (r *FileHandlerRouter) Delete(s *pssh.SSHServerConnSession, entry *utils.FileEntry) error {
75- handler, err := r.findHandler(entry.Filepath)
76- if err != nil {
77- return err
78- }
79- return handler.Delete(s, entry)
80-}
81-
82-// Rename moves a file in place when one handler serves both names.
83-func (r *FileHandlerRouter) Rename(s *pssh.SSHServerConnSession, from, to *utils.FileEntry) error {
84- handler, err := r.findHandler(from.Filepath)
85- if err != nil {
86- return err
87- }
88- target, err := r.findHandler(to.Filepath)
89- if err != nil {
90- return err
91- }
92- renamer, ok := handler.(utils.FileRenamer)
93- if !ok || handler != target {
94- return errors.ErrUnsupported
95- }
96- return renamer.Rename(s, from, to)
97-}
98-
99-func (r *FileHandlerRouter) Read(s *pssh.SSHServerConnSession, entry *utils.FileEntry) (os.FileInfo, utils.ReadAndReaderAtCloser, error) {
100- handler, err := r.findHandler(entry.Filepath)
101- if err != nil {
102- return nil, nil, err
103- }
104- return handler.Read(s, entry)
105-}
106-
107-func (r *FileHandlerRouter) List(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
108- files := []os.FileInfo{}
109- // Several extensions can share a handler, and handlers can list the
110- // same entries, such as the root directory.
111- listed := map[ReadWriteHandler]bool{}
112- seen := map[string]bool{}
113- for _, handler := range r.FileMap {
114- if listed[handler] {
115- continue
116- }
117- listed[handler] = true
118-
119- ff, err := handler.List(s, fpath, isDir, recursive)
120- if err != nil {
121- r.GetLogger(s).Error("handler list", "err", err)
122- continue
123- }
124- for _, f := range ff {
125- key := fmt.Sprintf("%s:%v", f.Name(), f.IsDir())
126- if !seen[key] {
127- seen[key] = true
128- files = append(files, f)
129- }
130- }
131- }
132- return files, nil
133-}
134-
135-func (r *FileHandlerRouter) GetLogger(s *pssh.SSHServerConnSession) *slog.Logger {
136- return pssh.GetLogger(s)
137-}
138-
139-func (r *FileHandlerRouter) Validate(s *pssh.SSHServerConnSession) error {
140- logger := pssh.GetLogger(s)
141- user := pssh.GetUser(s)
142-
143- if user == nil {
144- err := fmt.Errorf("could not get user from ctx")
145- logger.Error("error getting user from ctx", "err", err)
146- return err
147- }
148-
149- logger.Info(
150- "attempting to upload files",
151- "user", user.Name,
152- "space", r.Space,
153- )
154- return nil
155-}
156-
157-func BaseList(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool, spaces []string, dbpool db.PostDB) ([]os.FileInfo, error) {
158- var fileList []os.FileInfo
159- logger := pssh.GetLogger(s)
160- user := pssh.GetUser(s)
161-
162- var err error
163-
164- if user == nil {
165- err = fmt.Errorf("could not get user from ctx")
166- logger.Error("error getting user from ctx", "err", err)
167- return fileList, err
168- }
169-
170- cleanFilename := filepath.Base(fpath)
171-
172- var post *db.Post
173- var posts []*db.Post
174-
175- if cleanFilename == "" || cleanFilename == "." || cleanFilename == "/" {
176- name := cleanFilename
177- if name == "" {
178- name = "/"
179- }
180-
181- fileList = append(fileList, &utils.VirtualFile{
182- FName: name,
183- FIsDir: true,
184- })
185-
186- for _, space := range spaces {
187- curPosts, e := dbpool.FindAllPostsByUser(user.ID, space)
188- if e != nil {
189- err = e
190- break
191- }
192- posts = append(posts, curPosts...)
193- }
194- } else {
195- for _, space := range spaces {
196- p, e := dbpool.FindPostWithFilename(cleanFilename, user.ID, space)
197- if e != nil {
198- err = e
199- continue
200- }
201- post = p
202- }
203-
204- posts = append(posts, post)
205- }
206-
207- if err != nil && !errors.Is(err, sql.ErrNoRows) {
208- return nil, err
209- }
210-
211- for _, post := range posts {
212- if post == nil {
213- continue
214- }
215-
216- fileList = append(fileList, &utils.VirtualFile{
217- FName: post.Filename,
218- FIsDir: false,
219- FSize: int64(post.FileSize),
220- FModTime: *post.UpdatedAt,
221- })
222- }
223-
224- return fileList, nil
225-}
R pkg/filehandlers/posts.go => pkg/postfs/postfs.go
+73, -1
......@@ -1,4 +1,5 @@
1-package filehandlers
1+// Package postfs serves an app's posts as files over scp, sftp and sshfs.
2+package postfs
23
34 import (
45 "database/sql"
......@@ -242,3 +243,74 @@ func renamePost(dbpool db.PostDB, user *db.User, space, fromName, toName string,
242243 }
243244 return err
244245 }
246+
247+// ListPosts lists a user's posts in spaces, or the one at fpath.
248+func ListPosts(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool, spaces []string, dbpool db.PostDB) ([]os.FileInfo, error) {
249+ var fileList []os.FileInfo
250+ logger := pssh.GetLogger(s)
251+ user := pssh.GetUser(s)
252+
253+ var err error
254+
255+ if user == nil {
256+ err = fmt.Errorf("could not get user from ctx")
257+ logger.Error("error getting user from ctx", "err", err)
258+ return fileList, err
259+ }
260+
261+ cleanFilename := filepath.Base(fpath)
262+
263+ var post *db.Post
264+ var posts []*db.Post
265+
266+ if cleanFilename == "" || cleanFilename == "." || cleanFilename == "/" {
267+ name := cleanFilename
268+ if name == "" {
269+ name = "/"
270+ }
271+
272+ fileList = append(fileList, &sendutils.VirtualFile{
273+ FName: name,
274+ FIsDir: true,
275+ })
276+
277+ for _, space := range spaces {
278+ curPosts, e := dbpool.FindAllPostsByUser(user.ID, space)
279+ if e != nil {
280+ err = e
281+ break
282+ }
283+ posts = append(posts, curPosts...)
284+ }
285+ } else {
286+ for _, space := range spaces {
287+ p, e := dbpool.FindPostWithFilename(cleanFilename, user.ID, space)
288+ if e != nil {
289+ err = e
290+ continue
291+ }
292+ post = p
293+ }
294+
295+ posts = append(posts, post)
296+ }
297+
298+ if err != nil && !errors.Is(err, sql.ErrNoRows) {
299+ return nil, err
300+ }
301+
302+ for _, post := range posts {
303+ if post == nil {
304+ continue
305+ }
306+
307+ fileList = append(fileList, &sendutils.VirtualFile{
308+ FName: post.Filename,
309+ FIsDir: false,
310+ FSize: int64(post.FileSize),
311+ FModTime: *post.UpdatedAt,
312+ })
313+ }
314+
315+ return fileList, nil
316+}
R pkg/filehandlers/rename_test.go => pkg/postfs/rename_test.go
+1, -55
......@@ -1,4 +1,4 @@
1-package filehandlers
1+package postfs
22
33 import (
44 "context"
......@@ -41,20 +41,6 @@ func entry(name string) *sendutils.FileEntry {
4141 return &sendutils.FileEntry{Filepath: "/" + name}
4242 }
4343
44-// postsHandler stores posts with the shared post functions.
45-type postsHandler struct {
46- ReadWriteHandler
47- db *stub.PostsDB
48-}
49-
50-func (h *postsHandler) List(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
51- return BaseList(s, fpath, isDir, recursive, []string{"prose"}, h.db)
52-}
53-
54-func (h *postsHandler) Rename(s *pssh.SSHServerConnSession, from, to *sendutils.FileEntry) error {
55- return RenamePost(s, h.db, "prose", from, to, extRename(nil))
56-}
57-
5844 func newPostsSession(t *testing.T) (*pssh.SSHServerConnSession, *stub.PostsDB) {
5945 t.Helper()
6046 logger := slog.New(slog.DiscardHandler)
......@@ -129,46 +115,6 @@ func TestPostRename(t *testing.T) {
129115 }
130116 }
131117
132-// otherHandler is a handler of a different kind, like prose's images.
133-type otherHandler struct{ ReadWriteHandler }
134-
135-func (otherHandler) List(*pssh.SSHServerConnSession, string, bool, bool) ([]os.FileInfo, error) {
136- return []os.FileInfo{&sendutils.VirtualFile{FName: "/", FIsDir: true}, &sendutils.VirtualFile{FName: "x.png"}}, nil
137-}
138-
139-func TestRouterSharedHandler(t *testing.T) {
140- session, posts := newPostsSession(t)
141- postHandler := &postsHandler{db: posts}
142- router := NewFileHandlerRouter("prose", map[string]ReadWriteHandler{
143- ".md": postHandler,
144- ".lxt": postHandler,
145- ".txt": &postsHandler{db: posts},
146- "fallback": otherHandler{},
147- })
148- insertPost(t, posts, "a.md", "# a", false)
149-
150- list, err := router.List(session, "/", true, false)
151- if err != nil {
152- t.Fatal(err)
153- }
154- var names []string
155- for _, f := range list {
156- names = append(names, f.Name())
157- }
158- if len(names) != 3 {
159- t.Errorf("listed %q, want /, a.md and x.png once each", names)
160- }
161-
162- if err := router.Rename(session, entry("a.md"), entry("a.lxt")); err != nil {
163- t.Fatal(err)
164- }
165- for _, to := range []string{"a.txt", "a.png"} {
166- if err := router.Rename(session, entry("a.lxt"), entry(to)); !errors.Is(err, errors.ErrUnsupported) {
167- t.Errorf("rename to %s across handlers: %v", to, err)
168- }
169- }
170-}
171-
172118 func TestPostRenameRelations(t *testing.T) {
173119 session, posts := newPostsSession(t)
174120 a := insertPost(t, posts, "a.md", "# a", false)
R pkg/filehandlers/sftp_test.go => pkg/postfs/sftp_test.go
+1, -1
......@@ -1,4 +1,4 @@
1-package filehandlers_test
1+package postfs_test
22
33 import (
44 "context"