pico

created pr with pico/141.1 on 2026-09-26T01:18:41Z · by c8ef7d19
added pico/141.2 on 2026-09-26T01:40:03Z · by c8ef7d19
1: 2845ffb = 1: 2845ffb refactor: invite system for pgs and prose
-: ------- > 2: ee606d0 chore(prose): require plus or prose feature flag in order to upload
added pico/141.3 on 2026-09-26T14:41:34Z · by c8ef7d19
1: 2845ffb = 1: 2845ffb refactor: invite system for pgs and prose
2: ee606d0 = 2: ee606d0 chore(prose): require plus or prose feature flag in order to upload
-: ------- > 3: e14a99e refactor: remove unnecessary tests
added pico/141.4 on 2026-09-26T14:42:29Z · by c8ef7d19
1: 2845ffb = 1: 2845ffb refactor: invite system for pgs and prose
2: ee606d0 < -: ------- chore(prose): require plus or prose feature flag in order to upload
3: e14a99e ! 2: 7fb4f8f chore(prose): require plus or prose feature flag in order to upload
added pico/141.5 on 2026-09-26T15:11:30Z · by c8ef7d19
1: 2845ffb = 1: 2845ffb refactor: invite system for pgs and prose
2: 7fb4f8f ! 2: e469694 chore(prose): require plus or prose feature flag in order to upload
added pico/141.6 on 2026-10-01T13:39:16Z · by c8ef7d19
1: 2845ffb ! 1: 1e9cddc style(prose): minimal design ethos
2: e469694 ! 2: 0967882 chore(prose): remove with_styles and layout front-matter fields
cmds
checkout latest patchset:
ssh pr.pico.sh pull pico:141 | git am -3
checkout specific patchset revision:
ssh pr.pico.sh pull pico:141 [rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pico:141
add comment to patch request:
ssh pr.pico.sh comment pico:141 "lgtm!"
+1 -1 pkg/apps/prose/ssh.go #
......@@ -57,7 +57,7 @@ func StartSshServer() {
5757 ".lxt": filehandlers.NewScpPostHandler(dbh, cfg, hooks),
5858 "fallback": uploadimgs.NewUploadImgHandler(dbh, cfg, st),
5959 }
60- handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
60+ handler := NewUploadHandler(cfg, dbh, fileMap)
6161
6262 sshAuth := shared.NewSshAuthHandler(dbh, logger, "prose")
6363
+110 -0 pkg/apps/prose/uploader.go #
......@@ -0,0 +1,110 @@
1+package prose
2+
3+import (
4+ "fmt"
5+ "log/slog"
6+ "strings"
7+
8+ "github.com/picosh/pico/pkg/db"
9+ "github.com/picosh/pico/pkg/filehandlers"
10+ "github.com/picosh/pico/pkg/pssh"
11+ sendutils "github.com/picosh/pico/pkg/send/utils"
12+ "github.com/picosh/pico/pkg/shared"
13+)
14+
15+type ctxFeatureFlagKey struct{}
16+
17+func getFeatureFlag(s *pssh.SSHServerConnSession) *db.FeatureFlag {
18+ v := s.Context().Value(ctxFeatureFlagKey{})
19+ if v == nil {
20+ return nil
21+ }
22+ ff := s.Context().Value(ctxFeatureFlagKey{}).(*db.FeatureFlag)
23+ return ff
24+}
25+
26+func setFeatureFlag(s *pssh.SSHServerConnSession, ff *db.FeatureFlag) {
27+ s.SetValue(ctxFeatureFlagKey{}, ff)
28+}
29+
30+func setFeatureLimits(ff *db.FeatureFlag, cfg *shared.ConfigSite) {
31+ ff.Data.StorageMax = ff.FindStorageMax(cfg.MaxSize)
32+ ff.Data.FileMax = ff.FindFileMax(cfg.MaxAssetSize)
33+ ff.Data.SpecialFileMax = ff.FindSpecialFileMax(cfg.MaxSpecialFileSize)
34+}
35+
36+func findFeatureFlag(dbpool db.DB, cfg *shared.ConfigSite, userID string) (*db.FeatureFlag, error) {
37+ ff, err := dbpool.FindFeature(userID, "plus")
38+ if err == nil {
39+ if ff.IsValid() {
40+ setFeatureLimits(ff, cfg)
41+ return ff, nil
42+ }
43+ err = fmt.Errorf("ERROR: your pico+ has expired")
44+ }
45+
46+ ffProse, proseErr := dbpool.FindFeature(userID, "prose")
47+ if proseErr == nil {
48+ if ffProse.IsValid() {
49+ setFeatureLimits(ffProse, cfg)
50+ return ffProse, nil
51+ }
52+ proseErr = fmt.Errorf("ERROR: your prose access has expired")
53+ }
54+
55+ if err != nil && strings.Contains(err.Error(), "expired") {
56+ return nil, err
57+ }
58+ if proseErr != nil && strings.Contains(proseErr.Error(), "expired") {
59+ return nil, proseErr
60+ }
61+ return nil, fmt.Errorf("ERROR: uploading to prose requires the prose feature flag or pico+")
62+}
63+
64+type UploadHandler struct {
65+ *filehandlers.FileHandlerRouter
66+ Cfg *shared.ConfigSite
67+ DB db.DB
68+}
69+
70+var _ sendutils.CopyFromClientHandler = &UploadHandler{}
71+var _ sendutils.CopyFromClientHandler = (*UploadHandler)(nil)
72+
73+func NewUploadHandler(cfg *shared.ConfigSite, dbpool db.DB, fileMap map[string]filehandlers.ReadWriteHandler) *UploadHandler {
74+ router := filehandlers.NewFileHandlerRouter(cfg, dbpool, fileMap)
75+ return &UploadHandler{
76+ FileHandlerRouter: router,
77+ Cfg: cfg,
78+ DB: dbpool,
79+ }
80+}
81+
82+func (h *UploadHandler) GetLogger(s *pssh.SSHServerConnSession) *slog.Logger {
83+ logger := pssh.GetLogger(s)
84+ if logger == nil {
85+ if h.Cfg != nil && h.Cfg.Logger != nil {
86+ return h.Cfg.Logger
87+ }
88+ return slog.Default()
89+ }
90+ return logger
91+}
92+
93+func (h *UploadHandler) Validate(s *pssh.SSHServerConnSession) error {
94+ logger := h.GetLogger(s)
95+ user := pssh.GetUser(s)
96+
97+ if user == nil {
98+ err := fmt.Errorf("could not get user from ctx")
99+ logger.Error("error getting user from ctx", "err", err)
100+ return err
101+ }
102+
103+ ff, err := findFeatureFlag(h.DB, h.Cfg, user.ID)
104+ if err != nil {
105+ return err
106+ }
107+ setFeatureFlag(s, ff)
108+
109+ return h.FileHandlerRouter.Validate(s)
110+}
+110 -0 pkg/apps/prose/uploader_test.go #
......@@ -0,0 +1,110 @@
1+package prose
2+
3+import (
4+ "context"
5+ "fmt"
6+ "log/slog"
7+ "testing"
8+ "time"
9+
10+ "github.com/picosh/pico/pkg/db"
11+ "github.com/picosh/pico/pkg/db/stub"
12+ "github.com/picosh/pico/pkg/filehandlers"
13+ "github.com/picosh/pico/pkg/pssh"
14+ "github.com/picosh/pico/pkg/shared"
15+ "golang.org/x/crypto/ssh"
16+)
17+
18+type mockFeatureDB struct {
19+ *stub.StubDB
20+ features map[string]*db.FeatureFlag
21+}
22+
23+func newMockFeatureDB() *mockFeatureDB {
24+ return &mockFeatureDB{
25+ StubDB: stub.NewStubDB(slog.Default()),
26+ features: make(map[string]*db.FeatureFlag),
27+ }
28+}
29+
30+func (m *mockFeatureDB) FindFeature(userID, name string) (*db.FeatureFlag, error) {
31+ key := userID + ":" + name
32+ if ff, ok := m.features[key]; ok {
33+ return ff, nil
34+ }
35+ return nil, fmt.Errorf("feature flag %s not found for user %s", name, userID)
36+}
37+
38+func (m *mockFeatureDB) setFeature(userID string, ff *db.FeatureFlag) {
39+ key := userID + ":" + ff.Name
40+ m.features[key] = ff
41+}
42+
43+func TestUploadHandlerValidate(t *testing.T) {
44+ cfg := NewConfigSite("prose-test")
45+ validExpires := time.Now().Add(24 * time.Hour)
46+ userID := "user-1"
47+
48+ createSession := func(user *db.User) *pssh.SSHServerConnSession {
49+ conn := &pssh.SSHServerConn{
50+ Conn: &ssh.ServerConn{
51+ Permissions: &ssh.Permissions{
52+ Extensions: map[string]string{},
53+ },
54+ },
55+ Logger: slog.Default(),
56+ }
57+ sesh := &pssh.SSHServerConnSession{
58+ SSHServerConn: conn,
59+ Ctx: context.Background(),
60+ }
61+ if user != nil {
62+ pssh.SetUser(sesh, user)
63+ }
64+ return sesh
65+ }
66+
67+ t.Run("nil user in session fails validation", func(t *testing.T) {
68+ mockDB := newMockFeatureDB()
69+ handler := NewUploadHandler(cfg, mockDB, map[string]filehandlers.ReadWriteHandler{})
70+
71+ sesh := createSession(nil)
72+ err := handler.Validate(sesh)
73+ if err == nil {
74+ t.Fatalf("expected error for nil user, got nil")
75+ }
76+ })
77+
78+ t.Run("user without feature flag fails validation", func(t *testing.T) {
79+ mockDB := newMockFeatureDB()
80+ handler := NewUploadHandler(cfg, mockDB, map[string]filehandlers.ReadWriteHandler{})
81+
82+ user := &db.User{ID: userID, Name: "tester"}
83+ sesh := createSession(user)
84+ err := handler.Validate(sesh)
85+ if err == nil {
86+ t.Fatalf("expected error for user without feature flag, got nil")
87+ }
88+ })
89+
90+ t.Run("user with prose feature flag passes validation and sets session flag", func(t *testing.T) {
91+ mockDB := newMockFeatureDB()
92+ proseFF := db.NewFeatureFlag(userID, "prose", uint64(50*shared.MB), int64(5*shared.MB), int64(2*shared.KB))
93+ proseFF.ExpiresAt = &validExpires
94+ mockDB.setFeature(userID, proseFF)
95+
96+ handler := NewUploadHandler(cfg, mockDB, map[string]filehandlers.ReadWriteHandler{})
97+
98+ user := &db.User{ID: userID, Name: "tester"}
99+ sesh := createSession(user)
100+ err := handler.Validate(sesh)
101+ if err != nil {
102+ t.Fatalf("unexpected error: %v", err)
103+ }
104+
105+ ff := getFeatureFlag(sesh)
106+ if ff == nil || ff.Name != "prose" {
107+ t.Errorf("expected session feature flag 'prose', got %v", ff)
108+ }
109+ })
110+}
Back to top