- commit
- 3c82bf5
- parent
- 97ef41a
- author
- Eric Bower
- date
- 2024-01-30 01:24:05 +0000 UTC
refactor(imgs): merge into prose.sh (#71) feat(pgs): use image proxy for images BREAKING CHANGE: removed imgs.sh blog features
46 files changed,
+507,
-1489
+2,
-116
1@@ -1,121 +1,7 @@
2 package main
3
4-import (
5- "context"
6- "fmt"
7- "os"
8- "os/signal"
9- "syscall"
10- "time"
11-
12- "github.com/charmbracelet/promwish"
13- "github.com/charmbracelet/ssh"
14- "github.com/charmbracelet/wish"
15- bm "github.com/charmbracelet/wish/bubbletea"
16- lm "github.com/charmbracelet/wish/logging"
17- "github.com/picosh/pico/db/postgres"
18- uploadimgs "github.com/picosh/pico/filehandlers/imgs"
19- "github.com/picosh/pico/imgs"
20- "github.com/picosh/pico/shared"
21- "github.com/picosh/pico/shared/storage"
22- "github.com/picosh/pico/wish/cms"
23- "github.com/picosh/send/list"
24- "github.com/picosh/send/pipe"
25- "github.com/picosh/send/proxy"
26- "github.com/picosh/send/send/auth"
27- wishrsync "github.com/picosh/send/send/rsync"
28- "github.com/picosh/send/send/scp"
29- "github.com/picosh/send/send/sftp"
30- "github.com/picosh/send/send/utils"
31-)
32-
33-type SSHServer struct{}
34-
35-func (me *SSHServer) authHandler(ctx ssh.Context, key ssh.PublicKey) bool {
36- return true
37-}
38-
39-func createRouter(cfg *shared.ConfigSite, handler utils.CopyFromClientHandler) proxy.Router {
40- return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
41- return []wish.Middleware{
42- pipe.Middleware(handler, ""),
43- list.Middleware(handler),
44- scp.Middleware(handler),
45- wishrsync.Middleware(handler),
46- auth.Middleware(handler),
47- bm.Middleware(cms.Middleware(&cfg.ConfigCms, cfg)),
48- lm.Middleware(),
49- }
50- }
51-}
52-
53-func withProxy(cfg *shared.ConfigSite, handler utils.CopyFromClientHandler, otherMiddleware ...wish.Middleware) ssh.Option {
54- return func(server *ssh.Server) error {
55- err := sftp.SSHOption(handler)(server)
56- if err != nil {
57- return err
58- }
59-
60- return proxy.WithProxy(createRouter(cfg, handler), otherMiddleware...)(server)
61- }
62-}
63+import "github.com/picosh/pico/prose"
64
65 func main() {
66- host := shared.GetEnv("IMGS_HOST", "0.0.0.0")
67- port := shared.GetEnv("IMGS_SSH_PORT", "2222")
68- promPort := shared.GetEnv("IMGS_PROM_PORT", "9222")
69- cfg := imgs.NewConfigSite()
70- logger := cfg.Logger
71- dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
72- defer dbh.Close()
73-
74- var st storage.ObjectStorage
75- var err error
76- if cfg.MinioURL == "" {
77- st, err = storage.NewStorageFS(cfg.StorageDir)
78- } else {
79- st, err = storage.NewStorageMinio(cfg.MinioURL, cfg.MinioUser, cfg.MinioPass)
80- }
81-
82- if err != nil {
83- logger.Fatal(err)
84- }
85-
86- handler := uploadimgs.NewUploadImgHandler(
87- dbh,
88- cfg,
89- st,
90- )
91-
92- sshServer := &SSHServer{}
93- s, err := wish.NewServer(
94- wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
95- wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
96- wish.WithPublicKeyAuth(sshServer.authHandler),
97- withProxy(
98- cfg,
99- handler,
100- promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "imgs-ssh"),
101- ),
102- )
103- if err != nil {
104- logger.Fatal(err)
105- }
106-
107- done := make(chan os.Signal, 1)
108- signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
109- logger.Infof("Starting SSH server on %s:%s", host, port)
110- go func() {
111- if err = s.ListenAndServe(); err != nil {
112- logger.Fatal(err)
113- }
114- }()
115-
116- <-done
117- logger.Info("Stopping SSH server")
118- ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
119- defer func() { cancel() }()
120- if err := s.Shutdown(ctx); err != nil {
121- logger.Fatal(err)
122- }
123+ prose.StartSshServer()
124 }
+2,
-5
1@@ -9,7 +9,6 @@ import (
2
3 "github.com/picosh/pico/db"
4 "github.com/picosh/pico/db/postgres"
5- "github.com/picosh/pico/imgs"
6 "github.com/picosh/pico/shared"
7 "github.com/picosh/pico/wish/cms/config"
8 "go.uber.org/zap"
9@@ -96,9 +95,8 @@ func main() {
10 datesFixed := []string{}
11 logger.Info("updating dates")
12 for _, post := range posts {
13- linkify := imgs.NewImgsLinkify(post.Username)
14 if post.Space == "prose" {
15- parsed, err := shared.ParseText(post.Text, linkify)
16+ parsed, err := shared.ParseText(post.Text)
17 if err != nil {
18 logger.Error(err)
19 continue
20@@ -116,8 +114,7 @@ func main() {
21 }
22 }
23 } else if post.Space == "lists" {
24- linkify := imgs.NewImgsLinkify(post.Username)
25- parsed := shared.ListParseText(post.Text, linkify)
26+ parsed := shared.ListParseText(post.Text)
27 if err != nil {
28 logger.Error(err)
29 continue
1@@ -7,7 +7,6 @@ import (
2
3 "github.com/picosh/pico/db"
4 "github.com/picosh/pico/db/postgres"
5- "github.com/picosh/pico/imgs"
6 "github.com/picosh/pico/shared"
7 "github.com/picosh/pico/wish/cms/config"
8 "go.uber.org/zap"
9@@ -76,8 +75,7 @@ func main() {
10
11 logger.Info("replacing tags")
12 for _, post := range posts {
13- linkify := imgs.NewImgsLinkify(post.Username)
14- parsed, err := shared.ParseText(post.Text, linkify)
15+ parsed, err := shared.ParseText(post.Text)
16 if err != nil {
17 continue
18 }
+1,
-1
1@@ -135,7 +135,7 @@ func (f *Fetcher) Validate(lastDigest *time.Time, parsed *shared.ListParsedText)
2 func (f *Fetcher) RunPost(user *db.User, post *db.Post) error {
3 f.cfg.Logger.Infof("(%s) running feed post (%s)", user.Name, post.Filename)
4
5- parsed := shared.ListParseText(post.Text, shared.NewNullLinkify())
6+ parsed := shared.ListParseText(post.Text)
7
8 f.cfg.Logger.Infof("(%s) Last digest at (%s)", user.Name, post.Data.LastDigest)
9 err := f.Validate(post.Data.LastDigest, parsed)
+1,
-3
1@@ -10,7 +10,6 @@ import (
2 "github.com/charmbracelet/ssh"
3 "github.com/picosh/pico/db"
4 "github.com/picosh/pico/filehandlers"
5- "github.com/picosh/pico/imgs"
6 "github.com/picosh/pico/shared"
7 )
8
9@@ -42,8 +41,7 @@ func (p *FeedHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaData)
10 }
11
12 func (p *FeedHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData) error {
13- linkify := imgs.NewImgsLinkify(data.Username)
14- parsedText := shared.ListParseText(string(data.Text), linkify)
15+ parsedText := shared.ListParseText(string(data.Text))
16
17 if parsedText.Title == "" {
18 data.Title = shared.ToUpper(data.Slug)
+6,
-3
1@@ -33,7 +33,7 @@ func (me *SSHServer) authHandler(ctx ssh.Context, key ssh.PublicKey) bool {
2 return true
3 }
4
5-func createRouter(handler *filehandlers.ScpUploadHandler) proxy.Router {
6+func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
7 return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
8 return []wish.Middleware{
9 pipe.Middleware(handler, ".txt"),
10@@ -47,7 +47,7 @@ func createRouter(handler *filehandlers.ScpUploadHandler) proxy.Router {
11 }
12 }
13
14-func withProxy(handler *filehandlers.ScpUploadHandler, otherMiddleware ...wish.Middleware) ssh.Option {
15+func withProxy(handler *filehandlers.FileHandlerRouter, otherMiddleware ...wish.Middleware) ssh.Option {
16 return func(server *ssh.Server) error {
17 err := sftp.SSHOption(handler)(server)
18 if err != nil {
19@@ -84,7 +84,10 @@ func StartSshServer() {
20 logger.Fatal(err)
21 }
22
23- handler := filehandlers.NewScpPostHandler(dbh, cfg, hooks, st)
24+ fileMap := map[string]filehandlers.ReadWriteHandler{
25+ "fallback": filehandlers.NewScpPostHandler(dbh, cfg, hooks, st),
26+ }
27+ handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
28
29 sshServer := &SSHServer{}
30 s, err := wish.NewServer(
+7,
-24
1@@ -12,6 +12,7 @@ import (
2
3 "github.com/charmbracelet/ssh"
4 "github.com/picosh/pico/db"
5+ futil "github.com/picosh/pico/filehandlers/util"
6 "github.com/picosh/pico/shared"
7 "github.com/picosh/pico/shared/storage"
8 "github.com/picosh/pico/wish/cms/util"
9@@ -19,8 +20,6 @@ import (
10 "go.uber.org/zap"
11 )
12
13-type ctxUserKey struct{}
14-type ctxFeatureFlagKey struct{}
15 type ctxBucketKey struct{}
16 type ctxStorageSizeKey struct{}
17 type ctxProjectKey struct{}
18@@ -42,14 +41,6 @@ func getBucket(s ssh.Session) (storage.Bucket, error) {
19 return bucket, nil
20 }
21
22-func getFeatureFlag(s ssh.Session) (*db.FeatureFlag, error) {
23- ff := s.Context().Value(ctxFeatureFlagKey{}).(*db.FeatureFlag)
24- if ff.Name == "" {
25- return ff, fmt.Errorf("feature flag not set on `ssh.Context()` for connection")
26- }
27- return ff, nil
28-}
29-
30 func getStorageSize(s ssh.Session) uint64 {
31 return s.Context().Value(ctxStorageSizeKey{}).(uint64)
32 }
33@@ -61,14 +52,6 @@ func incrementStorageSize(s ssh.Session, fileSize int64) uint64 {
34 return nextStorageSize
35 }
36
37-func getUser(s ssh.Session) (*db.User, error) {
38- user := s.Context().Value(ctxUserKey{}).(*db.User)
39- if user == nil {
40- return user, fmt.Errorf("user not set on `ssh.Context()` for connection")
41- }
42- return user, nil
43-}
44-
45 type FileData struct {
46 *utils.FileEntry
47 Text []byte
48@@ -98,7 +81,7 @@ func (h *UploadAssetHandler) GetLogger() *zap.SugaredLogger {
49 }
50
51 func (h *UploadAssetHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileInfo, utils.ReaderAtCloser, error) {
52- user, err := getUser(s)
53+ user, err := futil.GetUser(s)
54 if err != nil {
55 return nil, nil, err
56 }
57@@ -132,7 +115,7 @@ func (h *UploadAssetHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.Fil
58 func (h *UploadAssetHandler) List(s ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
59 var fileList []os.FileInfo
60
61- user, err := getUser(s)
62+ user, err := futil.GetUser(s)
63 if err != nil {
64 return fileList, err
65 }
66@@ -204,7 +187,8 @@ func (h *UploadAssetHandler) Validate(s ssh.Session) error {
67 ff.Data.StorageMax = ff.FindStorageMax(h.Cfg.MaxSize)
68 ff.Data.FileMax = ff.FindFileMax(h.Cfg.MaxAssetSize)
69
70- s.Context().SetValue(ctxFeatureFlagKey{}, ff)
71+ futil.SetFeatureFlag(s, ff)
72+ futil.SetUser(s, user)
73
74 assetBucket := shared.GetAssetBucketName(user.ID)
75 bucket, err := h.Storage.UpsertBucket(assetBucket)
76@@ -220,14 +204,13 @@ func (h *UploadAssetHandler) Validate(s ssh.Session) error {
77 s.Context().SetValue(ctxStorageSizeKey{}, totalStorageSize)
78 h.Cfg.Logger.Infof("(%s) bucket size is current (%d bytes)", user.Name, totalStorageSize)
79
80- s.Context().SetValue(ctxUserKey{}, user)
81 h.Cfg.Logger.Infof("(%s) attempting to upload files to (%s)", user.Name, h.Cfg.Space)
82
83 return nil
84 }
85
86 func (h *UploadAssetHandler) Write(s ssh.Session, entry *utils.FileEntry) (string, error) {
87- user, err := getUser(s)
88+ user, err := futil.GetUser(s)
89 if err != nil {
90 h.Cfg.Logger.Error(err)
91 return "", err
92@@ -276,7 +259,7 @@ func (h *UploadAssetHandler) Write(s ssh.Session, entry *utils.FileEntry) (strin
93 }
94
95 storageSize := getStorageSize(s)
96- featureFlag, err := getFeatureFlag(s)
97+ featureFlag, err := futil.GetFeatureFlag(s)
98 if err != nil {
99 return "", err
100 }
+0,
-35
1@@ -1,35 +0,0 @@
2-package uploadimgs
3-
4-import (
5- "github.com/charmbracelet/ssh"
6- "github.com/picosh/pico/db"
7- "github.com/picosh/pico/imgs"
8- "github.com/picosh/pico/shared"
9- "github.com/picosh/pico/shared/storage"
10- "github.com/picosh/send/send/utils"
11-)
12-
13-type ImgsAPI struct {
14- Cfg *shared.ConfigSite
15- Db db.DB
16- St storage.ObjectStorage
17-}
18-
19-func NewImgsAPI(dbpool db.DB, st storage.ObjectStorage) *ImgsAPI {
20- cfg := imgs.NewConfigSite()
21- return &ImgsAPI{
22- Cfg: cfg,
23- Db: dbpool,
24- St: st,
25- }
26-}
27-
28-func (img *ImgsAPI) Upload(s ssh.Session, file *utils.FileEntry) (string, error) {
29- handler := NewUploadImgHandler(img.Db, img.Cfg, img.St)
30- err := handler.Validate(s)
31- if err != nil {
32- return "", err
33- }
34-
35- return handler.Write(s, file)
36-}
+13,
-123
1@@ -14,31 +14,13 @@ import (
2 "github.com/charmbracelet/ssh"
3 exifremove "github.com/neurosnap/go-exif-remove"
4 "github.com/picosh/pico/db"
5+ "github.com/picosh/pico/filehandlers/util"
6 "github.com/picosh/pico/shared"
7 "github.com/picosh/pico/shared/storage"
8- "github.com/picosh/pico/wish/cms/util"
9 "github.com/picosh/send/send/utils"
10- "go.uber.org/zap"
11 )
12
13-type ctxUserKey struct{}
14-type ctxFeatureFlagKey struct{}
15-
16-func getUser(s ssh.Session) (*db.User, error) {
17- user := s.Context().Value(ctxUserKey{}).(*db.User)
18- if user == nil {
19- return user, fmt.Errorf("user not set on `ssh.Context()` for connection")
20- }
21- return user, nil
22-}
23-
24-func getFeatureFlag(s ssh.Session) (*db.FeatureFlag, error) {
25- ff := s.Context().Value(ctxFeatureFlagKey{}).(*db.FeatureFlag)
26- if ff.Name == "" {
27- return ff, fmt.Errorf("feature flag not set on `ssh.Context()` for connection")
28- }
29- return ff, nil
30-}
31+var Space = "imgs"
32
33 type PostMetaData struct {
34 *db.Post
35@@ -81,12 +63,8 @@ func (h *UploadImgHandler) removePost(data *PostMetaData) error {
36 return nil
37 }
38
39-func (h *UploadImgHandler) GetLogger() *zap.SugaredLogger {
40- return h.Cfg.Logger
41-}
42-
43 func (h *UploadImgHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileInfo, utils.ReaderAtCloser, error) {
44- user, err := getUser(s)
45+ user, err := util.GetUser(s)
46 if err != nil {
47 return nil, nil, err
48 }
49@@ -97,7 +75,7 @@ func (h *UploadImgHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileI
50 return nil, nil, os.ErrNotExist
51 }
52
53- post, err := h.DBPool.FindPostWithFilename(cleanFilename, user.ID, h.Cfg.Space)
54+ post, err := h.DBPool.FindPostWithFilename(cleanFilename, user.ID, Space)
55 if err != nil {
56 return nil, nil, err
57 }
58@@ -124,91 +102,8 @@ func (h *UploadImgHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileI
59 return fileInfo, reader, nil
60 }
61
62-func (h *UploadImgHandler) List(s ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
63- var fileList []os.FileInfo
64- user, err := getUser(s)
65- if err != nil {
66- return fileList, err
67- }
68- cleanFilename := filepath.Base(fpath)
69-
70- var post *db.Post
71- var posts []*db.Post
72-
73- if cleanFilename == "" || cleanFilename == "." || cleanFilename == "/" {
74- name := cleanFilename
75- if name == "" {
76- name = "/"
77- }
78-
79- fileList = append(fileList, &utils.VirtualFile{
80- FName: name,
81- FIsDir: true,
82- })
83-
84- posts, err = h.DBPool.FindAllPostsForUser(user.ID, h.Cfg.Space)
85- } else {
86- post, err = h.DBPool.FindPostWithFilename(cleanFilename, user.ID, h.Cfg.Space)
87-
88- posts = append(posts, post)
89- }
90-
91- if err != nil {
92- return nil, err
93- }
94-
95- for _, post := range posts {
96- fileList = append(fileList, &utils.VirtualFile{
97- FName: post.Filename,
98- FIsDir: false,
99- FSize: int64(post.FileSize),
100- FModTime: *post.UpdatedAt,
101- })
102- }
103-
104- return fileList, nil
105-}
106-
107-func (h *UploadImgHandler) Validate(s ssh.Session) error {
108- var err error
109- key, err := util.KeyText(s)
110- if err != nil {
111- return fmt.Errorf("key not found")
112- }
113-
114- user, err := h.DBPool.FindUserForKey(s.User(), key)
115- if err != nil {
116- return err
117- }
118-
119- if user.Name == "" {
120- return fmt.Errorf("must have username set")
121- }
122-
123- ff, _ := h.DBPool.FindFeatureForUser(user.ID, "imgs")
124- // imgs.sh has a free tier so users might not have a feature flag
125- // in which case we set sane defaults
126- if ff == nil {
127- ff = db.NewFeatureFlag(
128- user.ID,
129- "imgs",
130- h.Cfg.MaxSize,
131- h.Cfg.MaxAssetSize,
132- )
133- }
134- // this is jank
135- ff.Data.StorageMax = ff.FindStorageMax(h.Cfg.MaxSize)
136- ff.Data.FileMax = ff.FindFileMax(h.Cfg.MaxAssetSize)
137-
138- s.Context().SetValue(ctxFeatureFlagKey{}, ff)
139-
140- s.Context().SetValue(ctxUserKey{}, user)
141- h.Cfg.Logger.Infof("(%s) attempting to upload files to (%s)", user.Name, h.Cfg.Space)
142- return nil
143-}
144-
145 func (h *UploadImgHandler) Write(s ssh.Session, entry *utils.FileEntry) (string, error) {
146- user, err := getUser(s)
147+ user, err := util.GetUser(s)
148 if err != nil {
149 h.Cfg.Logger.Error(err)
150 return "", err
151@@ -221,6 +116,10 @@ func (h *UploadImgHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
152 text = b
153 }
154 mimeType := http.DetectContentType(text)
155+ ext := filepath.Ext(filename)
156+ if ext == ".svg" {
157+ mimeType = "image/svg+xml"
158+ }
159 // strip exif data
160 if slices.Contains([]string{"image/png", "image/jpg", "image/jpeg"}, mimeType) {
161 noExifBytes, err := exifremove.Remove(text)
162@@ -237,9 +136,9 @@ func (h *UploadImgHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
163 }
164
165 now := time.Now()
166- slug := shared.SanitizeFileExt(filename)
167 fileSize := binary.Size(text)
168 shasum := shared.Shasum(text)
169+ slug := shared.SanitizeFileExt(filename)
170
171 nextPost := db.Post{
172 Filename: filename,
173@@ -251,25 +150,16 @@ func (h *UploadImgHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
174 Shasum: shasum,
175 }
176
177- ext := filepath.Ext(filename)
178- // DetectContentType does not detect markdown
179- if ext == ".md" {
180- nextPost.MimeType = "text/markdown; charset=UTF-8"
181- // DetectContentType does not detect image/svg
182- } else if ext == ".svg" {
183- nextPost.MimeType = "image/svg+xml"
184- }
185-
186 post, err := h.DBPool.FindPostWithFilename(
187 nextPost.Filename,
188 user.ID,
189- h.Cfg.Space,
190+ Space,
191 )
192 if err != nil {
193 h.Cfg.Logger.Infof("(%s) unable to find image (%s), continuing", nextPost.Filename, err)
194 }
195
196- featureFlag, err := getFeatureFlag(s)
197+ featureFlag, err := util.GetFeatureFlag(s)
198 if err != nil {
199 return "", err
200 }
201@@ -302,7 +192,7 @@ func (h *UploadImgHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
202 url := h.Cfg.FullPostURL(
203 curl,
204 user.Name,
205- metadata.Slug,
206+ metadata.Filename,
207 )
208 maxSize := int(featureFlag.Data.StorageMax)
209 str := fmt.Sprintf(
+3,
-2
1@@ -8,6 +8,7 @@ import (
2
3 "github.com/charmbracelet/ssh"
4 "github.com/picosh/pico/db"
5+ "github.com/picosh/pico/filehandlers/util"
6 "github.com/picosh/pico/shared"
7 "github.com/picosh/send/send/utils"
8 )
9@@ -79,7 +80,7 @@ func (h *UploadImgHandler) writeImg(s ssh.Session, data *PostMetaData) error {
10 if !valid {
11 return err
12 }
13- user, err := getUser(s)
14+ user, err := util.GetUser(s)
15 if err != nil {
16 return err
17 }
18@@ -110,7 +111,7 @@ func (h *UploadImgHandler) writeImg(s ssh.Session, data *PostMetaData) error {
19 h.Cfg.Logger.Infof("(%s) not found, adding record", data.Filename)
20 insertPost := db.Post{
21 UserID: user.ID,
22- Space: h.Cfg.Space,
23+ Space: Space,
24
25 Data: data.Data,
26 Description: data.Description,
+9,
-100
1@@ -12,24 +12,12 @@ import (
2
3 "github.com/charmbracelet/ssh"
4 "github.com/picosh/pico/db"
5- uploadimgs "github.com/picosh/pico/filehandlers/imgs"
6+ "github.com/picosh/pico/filehandlers/util"
7 "github.com/picosh/pico/shared"
8 "github.com/picosh/pico/shared/storage"
9- "github.com/picosh/pico/wish/cms/util"
10 "github.com/picosh/send/send/utils"
11- "go.uber.org/zap"
12 )
13
14-type ctxUserKey struct{}
15-
16-func getUser(s ssh.Session) (*db.User, error) {
17- user := s.Context().Value(ctxUserKey{}).(*db.User)
18- if user == nil {
19- return user, fmt.Errorf("user not set on `ssh.Context()` for connection")
20- }
21- return user, nil
22-}
23-
24 type PostMetaData struct {
25 *db.Post
26 Cur *db.Post
27@@ -45,29 +33,21 @@ type ScpFileHooks interface {
28 }
29
30 type ScpUploadHandler struct {
31- DBPool db.DB
32- Cfg *shared.ConfigSite
33- Hooks ScpFileHooks
34- ImgClient *uploadimgs.ImgsAPI
35+ DBPool db.DB
36+ Cfg *shared.ConfigSite
37+ Hooks ScpFileHooks
38 }
39
40 func NewScpPostHandler(dbpool db.DB, cfg *shared.ConfigSite, hooks ScpFileHooks, st storage.ObjectStorage) *ScpUploadHandler {
41- client := uploadimgs.NewImgsAPI(dbpool, st)
42-
43 return &ScpUploadHandler{
44- DBPool: dbpool,
45- Cfg: cfg,
46- Hooks: hooks,
47- ImgClient: client,
48+ DBPool: dbpool,
49+ Cfg: cfg,
50+ Hooks: hooks,
51 }
52 }
53
54-func (h *ScpUploadHandler) GetLogger() *zap.SugaredLogger {
55- return h.Cfg.Logger
56-}
57-
58 func (h *ScpUploadHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileInfo, utils.ReaderAtCloser, error) {
59- user, err := getUser(s)
60+ user, err := util.GetUser(s)
61 if err != nil {
62 return nil, nil, err
63 }
64@@ -94,76 +74,9 @@ func (h *ScpUploadHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileI
65 return fileInfo, reader, nil
66 }
67
68-func (h *ScpUploadHandler) List(s ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
69- var fileList []os.FileInfo
70- user, err := getUser(s)
71- if err != nil {
72- return fileList, err
73- }
74-
75- cleanFilename := filepath.Base(fpath)
76-
77- var post *db.Post
78- var posts []*db.Post
79-
80- if cleanFilename == "" || cleanFilename == "." || cleanFilename == "/" {
81- name := cleanFilename
82- if name == "" {
83- name = "/"
84- }
85-
86- fileList = append(fileList, &utils.VirtualFile{
87- FName: name,
88- FIsDir: true,
89- })
90-
91- posts, err = h.DBPool.FindAllPostsForUser(user.ID, h.Cfg.Space)
92- } else {
93- post, err = h.DBPool.FindPostWithFilename(cleanFilename, user.ID, h.Cfg.Space)
94-
95- posts = append(posts, post)
96- }
97-
98- if err != nil {
99- return nil, err
100- }
101-
102- for _, post := range posts {
103- fileList = append(fileList, &utils.VirtualFile{
104- FName: post.Filename,
105- FIsDir: false,
106- FSize: int64(post.FileSize),
107- FModTime: *post.UpdatedAt,
108- })
109- }
110-
111- return fileList, nil
112-}
113-
114-func (h *ScpUploadHandler) Validate(s ssh.Session) error {
115- var err error
116- key, err := util.KeyText(s)
117- if err != nil {
118- return fmt.Errorf("key not found")
119- }
120-
121- user, err := h.DBPool.FindUserForKey(s.User(), key)
122- if err != nil {
123- return err
124- }
125-
126- if user.Name == "" {
127- return fmt.Errorf("must have username set")
128- }
129-
130- s.Context().SetValue(ctxUserKey{}, user)
131- h.Cfg.Logger.Infof("(%s) attempting to upload files to (%s)", user.Name, h.Cfg.Space)
132- return nil
133-}
134-
135 func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string, error) {
136 logger := h.Cfg.Logger
137- user, err := getUser(s)
138+ user, err := util.GetUser(s)
139 if err != nil {
140 logger.Error(err)
141 return "", err
142@@ -172,10 +85,6 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
143 userID := user.ID
144 filename := filepath.Base(entry.Filepath)
145
146- if shared.IsExtAllowed(filename, h.ImgClient.Cfg.AllowedExt) {
147- return h.ImgClient.Upload(s, entry)
148- }
149-
150 var origText []byte
151 if b, err := io.ReadAll(entry.Reader); err == nil {
152 origText = b
+168,
-0
1@@ -0,0 +1,168 @@
2+package filehandlers
3+
4+import (
5+ "fmt"
6+ "os"
7+ "path/filepath"
8+
9+ "github.com/charmbracelet/ssh"
10+ "github.com/picosh/pico/db"
11+ "github.com/picosh/pico/filehandlers/util"
12+ "github.com/picosh/pico/shared"
13+ "github.com/picosh/send/send/utils"
14+ "go.uber.org/zap"
15+)
16+
17+type ReadWriteHandler interface {
18+ Write(ssh.Session, *utils.FileEntry) (string, error)
19+ Read(ssh.Session, *utils.FileEntry) (os.FileInfo, utils.ReaderAtCloser, error)
20+}
21+
22+type FileHandlerRouter struct {
23+ FileMap map[string]ReadWriteHandler
24+ Cfg *shared.ConfigSite
25+ DBPool db.DB
26+ Spaces []string
27+}
28+
29+var _ utils.CopyFromClientHandler = &FileHandlerRouter{} // Verify implementation
30+var _ utils.CopyFromClientHandler = (*FileHandlerRouter)(nil) // Verify implementation
31+
32+func NewFileHandlerRouter(cfg *shared.ConfigSite, dbpool db.DB, mapper map[string]ReadWriteHandler) *FileHandlerRouter {
33+ return &FileHandlerRouter{
34+ Cfg: cfg,
35+ DBPool: dbpool,
36+ FileMap: mapper,
37+ Spaces: []string{cfg.Space},
38+ }
39+}
40+
41+func (r *FileHandlerRouter) findHandler(entry *utils.FileEntry) (ReadWriteHandler, error) {
42+ fext := filepath.Ext(entry.Filepath)
43+ handler, ok := r.FileMap[fext]
44+ if !ok {
45+ hand, hasFallback := r.FileMap["fallback"]
46+ if !hasFallback {
47+ return nil, fmt.Errorf("no corresponding handler for file extension: %s", fext)
48+ }
49+ handler = hand
50+ }
51+ return handler, nil
52+}
53+
54+func (r *FileHandlerRouter) Write(s ssh.Session, entry *utils.FileEntry) (string, error) {
55+ handler, err := r.findHandler(entry)
56+ if err != nil {
57+ return "", err
58+ }
59+ return handler.Write(s, entry)
60+}
61+
62+func (r *FileHandlerRouter) Read(s ssh.Session, entry *utils.FileEntry) (os.FileInfo, utils.ReaderAtCloser, error) {
63+ handler, err := r.findHandler(entry)
64+ if err != nil {
65+ return nil, nil, err
66+ }
67+ return handler.Read(s, entry)
68+}
69+
70+func (r *FileHandlerRouter) List(s ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
71+ var fileList []os.FileInfo
72+ user, err := util.GetUser(s)
73+ if err != nil {
74+ return fileList, err
75+ }
76+ cleanFilename := filepath.Base(fpath)
77+
78+ var post *db.Post
79+ var posts []*db.Post
80+
81+ if cleanFilename == "" || cleanFilename == "." || cleanFilename == "/" {
82+ name := cleanFilename
83+ if name == "" {
84+ name = "/"
85+ }
86+
87+ fileList = append(fileList, &utils.VirtualFile{
88+ FName: name,
89+ FIsDir: true,
90+ })
91+
92+ for _, space := range r.Spaces {
93+ curPosts, e := r.DBPool.FindAllPostsForUser(user.ID, space)
94+ if e != nil {
95+ err = e
96+ break
97+ }
98+ posts = append(posts, curPosts...)
99+ }
100+ } else {
101+ for _, space := range r.Spaces {
102+ p, e := r.DBPool.FindPostWithFilename(cleanFilename, user.ID, space)
103+ if e != nil {
104+ err = e
105+ continue
106+ }
107+ post = p
108+ }
109+
110+ posts = append(posts, post)
111+ }
112+
113+ if err != nil {
114+ return nil, err
115+ }
116+
117+ for _, post := range posts {
118+ fileList = append(fileList, &utils.VirtualFile{
119+ FName: post.Filename,
120+ FIsDir: false,
121+ FSize: int64(post.FileSize),
122+ FModTime: *post.UpdatedAt,
123+ })
124+ }
125+
126+ return fileList, nil
127+}
128+
129+func (r *FileHandlerRouter) GetLogger() *zap.SugaredLogger {
130+ return r.Cfg.Logger
131+}
132+
133+func (r *FileHandlerRouter) Validate(s ssh.Session) error {
134+ var err error
135+ key, err := utils.KeyText(s)
136+ if err != nil {
137+ return fmt.Errorf("key not found")
138+ }
139+
140+ user, err := r.DBPool.FindUserForKey(s.User(), key)
141+ if err != nil {
142+ return err
143+ }
144+
145+ if user.Name == "" {
146+ return fmt.Errorf("must have username set")
147+ }
148+
149+ ff, _ := r.DBPool.FindFeatureForUser(user.ID, r.Cfg.Space)
150+ // we have free tiers so users might not have a feature flag
151+ // in which case we set sane defaults
152+ if ff == nil {
153+ ff = db.NewFeatureFlag(
154+ user.ID,
155+ r.Cfg.Space,
156+ r.Cfg.MaxSize,
157+ r.Cfg.MaxAssetSize,
158+ )
159+ }
160+ // this is jank
161+ ff.Data.StorageMax = ff.FindStorageMax(r.Cfg.MaxSize)
162+ ff.Data.FileMax = ff.FindFileMax(r.Cfg.MaxAssetSize)
163+
164+ util.SetUser(s, user)
165+ util.SetFeatureFlag(s, ff)
166+
167+ r.Cfg.Logger.Infof("(%s) attempting to upload files to (%s)", user.Name, r.Cfg.Space)
168+ return nil
169+}
+35,
-0
1@@ -0,0 +1,35 @@
2+package util
3+
4+import (
5+ "fmt"
6+
7+ "github.com/charmbracelet/ssh"
8+ "github.com/picosh/pico/db"
9+)
10+
11+type ctxUserKey struct{}
12+type ctxFeatureFlagKey struct{}
13+
14+func GetUser(s ssh.Session) (*db.User, error) {
15+ user := s.Context().Value(ctxUserKey{}).(*db.User)
16+ if user == nil {
17+ return user, fmt.Errorf("user not set on `ssh.Context()` for connection")
18+ }
19+ return user, nil
20+}
21+
22+func SetUser(s ssh.Session, user *db.User) {
23+ s.Context().SetValue(ctxUserKey{}, user)
24+}
25+
26+func GetFeatureFlag(s ssh.Session) (*db.FeatureFlag, error) {
27+ ff := s.Context().Value(ctxFeatureFlagKey{}).(*db.FeatureFlag)
28+ if ff.Name == "" {
29+ return ff, fmt.Errorf("feature flag not set on `ssh.Context()` for connection")
30+ }
31+ return ff, nil
32+}
33+
34+func SetFeatureFlag(s ssh.Session, ff *db.FeatureFlag) {
35+ s.Context().SetValue(ctxFeatureFlagKey{}, ff)
36+}
+63,
-517
1@@ -4,7 +4,6 @@ import (
2 "bytes"
3 "fmt"
4 "html/template"
5- "io"
6 "net/http"
7 "net/url"
8 "path/filepath"
9@@ -12,21 +11,15 @@ import (
10
11 _ "net/http/pprof"
12
13- "slices"
14-
15 "github.com/gorilla/feeds"
16 gocache "github.com/patrickmn/go-cache"
17 "github.com/picosh/pico/db"
18 "github.com/picosh/pico/db/postgres"
19+ "github.com/picosh/pico/pgs"
20 "github.com/picosh/pico/shared"
21 "github.com/picosh/pico/shared/storage"
22- "go.uber.org/zap"
23 )
24
25-type PageData struct {
26- Site shared.SitePageData
27-}
28-
29 type PostItemData struct {
30 BlogURL template.URL
31 URL template.URL
32@@ -36,415 +29,18 @@ type PostItemData struct {
33 Caption string
34 }
35
36-type BlogPageData struct {
37- Site shared.SitePageData
38- PageTitle string
39- URL template.URL
40- RSSURL template.URL
41- Username string
42- Readme *ReadmeTxt
43- Header *HeaderTxt
44- Posts []*PostItemData
45- HasFilter bool
46-}
47-
48 type PostPageData struct {
49- Site shared.SitePageData
50- PageTitle string
51- URL template.URL
52- BlogURL template.URL
53- Slug string
54- Title string
55- Caption string
56- Contents template.HTML
57- Text string
58- Username string
59- BlogName string
60- PublishAtISO string
61- PublishAt string
62- Tags []Link
63- ImgURL template.URL
64- PrevPage template.URL
65- NextPage template.URL
66-}
67-
68-type TransparencyPageData struct {
69- Site shared.SitePageData
70- Analytics *db.Analytics
71-}
72-
73-type Link struct {
74- URL template.URL
75- Text string
76-}
77-
78-type HeaderTxt struct {
79- Title string
80- Bio string
81- Nav []Link
82- HasLinks bool
83-}
84-
85-type ReadmeTxt struct {
86- HasText bool
87- Contents template.HTML
88-}
89-
90-func GetPostTitle(post *db.Post) string {
91- if post.Description == "" {
92- return post.Title
93- }
94-
95- return fmt.Sprintf("%s: %s", post.Title, post.Description)
96-}
97-
98-func GetBlogName(username string) string {
99- return username
100-}
101-
102-func blogHandler(w http.ResponseWriter, r *http.Request) {
103- username := shared.GetUsernameFromRequest(r)
104- dbpool := shared.GetDB(r)
105- logger := shared.GetLogger(r)
106- cfg := shared.GetCfg(r)
107-
108- user, err := dbpool.FindUserForName(username)
109- if err != nil {
110- logger.Infof("blog not found: %s", username)
111- http.Error(w, "blog not found", http.StatusNotFound)
112- return
113- }
114-
115- tag := r.URL.Query().Get("tag")
116- var posts []*db.Post
117- var p *db.Paginate[*db.Post]
118- pager := &db.Pager{Num: 1000, Page: 0}
119- if tag == "" {
120- p, err = dbpool.FindPostsForUser(pager, user.ID, cfg.Space)
121- } else {
122- p, err = dbpool.FindUserPostsByTag(pager, tag, user.ID, cfg.Space)
123- }
124- posts = p.Data
125-
126- if err != nil {
127- logger.Error(err)
128- http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError)
129- return
130- }
131-
132- ts, err := shared.RenderTemplate(cfg, []string{
133- cfg.StaticPath("html/blog.page.tmpl"),
134- })
135-
136- if err != nil {
137- logger.Error(err)
138- http.Error(w, err.Error(), http.StatusInternalServerError)
139- return
140- }
141-
142- headerTxt := &HeaderTxt{
143- Title: GetBlogName(username),
144- Bio: "",
145- }
146- readmeTxt := &ReadmeTxt{}
147-
148- curl := shared.CreateURLFromRequest(cfg, r)
149- postCollection := make([]*PostItemData, 0, len(posts))
150- for _, post := range posts {
151- url := fmt.Sprintf(
152- "%s/300x",
153- cfg.ImgURL(curl, post.Username, post.Slug),
154- )
155- postCollection = append(postCollection, &PostItemData{
156- ImgURL: template.URL(url),
157- URL: template.URL(cfg.ImgPostURL(curl, post.Username, post.Slug)),
158- Caption: post.Title,
159- PublishAt: post.PublishAt.Format("02 Jan, 2006"),
160- PublishAtISO: post.PublishAt.Format(time.RFC3339),
161- })
162- }
163-
164- data := BlogPageData{
165- Site: *cfg.GetSiteData(),
166- PageTitle: headerTxt.Title,
167- URL: template.URL(cfg.FullBlogURL(curl, username)),
168- RSSURL: template.URL(cfg.RssBlogURL(curl, username, tag)),
169- Readme: readmeTxt,
170- Header: headerTxt,
171- Username: username,
172- Posts: postCollection,
173- HasFilter: tag != "",
174- }
175-
176- err = ts.Execute(w, data)
177- if err != nil {
178- logger.Error(err)
179- http.Error(w, err.Error(), http.StatusInternalServerError)
180- }
181-}
182-
183-type ImgHandler struct {
184- Username string
185- Subdomain string
186- Slug string
187- Cfg *shared.ConfigSite
188- Dbpool db.DB
189- Storage storage.ObjectStorage
190- Logger *zap.SugaredLogger
191- Cache *gocache.Cache
192- Ratio *storage.Ratio
193- Original bool
194-}
195-
196-func imgHandler(w http.ResponseWriter, h *ImgHandler) {
197- user, err := h.Dbpool.FindUserForName(h.Username)
198- if err != nil {
199- h.Logger.Infof("blog not found: %s", h.Username)
200- http.Error(w, "blog not found", http.StatusNotFound)
201- return
202- }
203-
204- post, err := h.Dbpool.FindPostWithSlug(h.Slug, user.ID, h.Cfg.Space)
205- if err != nil {
206- errMsg := fmt.Sprintf("image not found %s/%s", h.Username, h.Slug)
207- h.Logger.Infof(errMsg)
208- http.Error(w, errMsg, http.StatusNotFound)
209- return
210- }
211-
212- _, err = h.Dbpool.AddViewCount(post.ID)
213- if err != nil {
214- h.Logger.Error(err)
215- }
216-
217- bucket, err := h.Storage.GetBucket(user.ID)
218- if err != nil {
219- h.Logger.Infof("bucket not found %s/%s", h.Username, post.Filename)
220- http.Error(w, err.Error(), http.StatusInternalServerError)
221- return
222- }
223-
224- contents, contentType, err := h.Storage.ServeFile(bucket, post.Filename, h.Ratio, h.Original, h.Cfg.UseImgProxy)
225- if err != nil {
226- h.Logger.Infof(
227- "file not found %s/%s in storage (bucket: %s, name: %s)",
228- h.Username,
229- post.Filename,
230- bucket.Name,
231- post.Filename,
232- )
233- http.Error(w, err.Error(), http.StatusInternalServerError)
234- return
235- }
236- defer contents.Close()
237-
238- w.Header().Add("Content-Type", contentType)
239-
240- _, err = io.Copy(w, contents)
241-
242- if err != nil {
243- h.Logger.Error(err)
244- }
245-}
246-
247-func imgRequestOriginal(w http.ResponseWriter, r *http.Request) {
248- username := shared.GetUsernameFromRequest(r)
249- subdomain := shared.GetSubdomain(r)
250- cfg := shared.GetCfg(r)
251-
252- var slug string
253- if !cfg.IsSubdomains() || subdomain == "" {
254- slug, _ = url.PathUnescape(shared.GetField(r, 1))
255- } else {
256- slug, _ = url.PathUnescape(shared.GetField(r, 0))
257- }
258-
259- // users might add the file extension when requesting an image
260- // but we want to remove that
261- slug = shared.SanitizeFileExt(slug)
262-
263- dbpool := shared.GetDB(r)
264- st := shared.GetStorage(r)
265- logger := shared.GetLogger(r)
266- cache := shared.GetCache(r)
267-
268- imgHandler(w, &ImgHandler{
269- Username: username,
270- Subdomain: subdomain,
271- Slug: slug,
272- Cfg: cfg,
273- Dbpool: dbpool,
274- Storage: st,
275- Logger: logger,
276- Cache: cache,
277- Original: true,
278- })
279-}
280-
281-func imgRequest(w http.ResponseWriter, r *http.Request) {
282- username := shared.GetUsernameFromRequest(r)
283- subdomain := shared.GetSubdomain(r)
284- cfg := shared.GetCfg(r)
285-
286- var dimes string
287- var slug string
288- if !cfg.IsSubdomains() || subdomain == "" {
289- slug, _ = url.PathUnescape(shared.GetField(r, 1))
290- dimes, _ = url.PathUnescape(shared.GetField(r, 2))
291- } else {
292- slug, _ = url.PathUnescape(shared.GetField(r, 0))
293- dimes, _ = url.PathUnescape(shared.GetField(r, 1))
294- }
295-
296- ratio, _ := storage.GetRatio(dimes)
297-
298- ext := filepath.Ext(slug)
299- // Files can contain periods. `filepath.Ext` is greedy and will clip the last period in the slug
300- // and call that a file extension so we want to be explicit about what
301- // file extensions we clip here
302- for _, fext := range cfg.AllowedExt {
303- if ext == fext {
304- // users might add the file extension when requesting an image
305- // but we want to remove that
306- slug = shared.SanitizeFileExt(slug)
307- break
308- }
309- }
310-
311- dbpool := shared.GetDB(r)
312- st := shared.GetStorage(r)
313- logger := shared.GetLogger(r)
314- cache := shared.GetCache(r)
315-
316- imgHandler(w, &ImgHandler{
317- Username: username,
318- Subdomain: subdomain,
319- Slug: slug,
320- Cfg: cfg,
321- Dbpool: dbpool,
322- Storage: st,
323- Logger: logger,
324- Cache: cache,
325- Ratio: ratio,
326- })
327+ ImgURL template.URL
328 }
329
330-func postHandler(w http.ResponseWriter, r *http.Request) {
331- username := shared.GetUsernameFromRequest(r)
332- subdomain := shared.GetSubdomain(r)
333- cfg := shared.GetCfg(r)
334+var Space = "imgs"
335
336- var slug string
337- if !cfg.IsSubdomains() || subdomain == "" {
338- slug, _ = url.PathUnescape(shared.GetField(r, 1))
339- } else {
340- slug, _ = url.PathUnescape(shared.GetField(r, 0))
341- }
342-
343- dbpool := shared.GetDB(r)
344- logger := shared.GetLogger(r)
345-
346- user, err := dbpool.FindUserForName(username)
347- if err != nil {
348- logger.Infof("blog not found: %s", username)
349- http.Error(w, "blog not found", http.StatusNotFound)
350- return
351- }
352-
353- blogName := GetBlogName(username)
354- curl := shared.CreateURLFromRequest(cfg, r)
355-
356- var data PostPageData
357- post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
358- if err == nil {
359- linkify := NewImgsLinkify(username)
360- parsed, err := shared.ParseText(post.Text, linkify)
361- if err != nil {
362- logger.Error(err)
363- }
364- text := ""
365- if parsed != nil {
366- text = parsed.Html
367- }
368-
369- tagLinks := make([]Link, 0, len(post.Tags))
370- for _, tag := range post.Tags {
371- tagLinks = append(tagLinks, Link{
372- URL: template.URL(cfg.TagURL(curl, username, tag)),
373- Text: tag,
374- })
375- }
376-
377- data = PostPageData{
378- Site: *cfg.GetSiteData(),
379- PageTitle: GetPostTitle(post),
380- URL: template.URL(cfg.FullPostURL(curl, post.Username, post.Slug)),
381- BlogURL: template.URL(cfg.FullBlogURL(curl, username)),
382- Caption: post.Description,
383- Title: post.Title,
384- Slug: post.Slug,
385- PublishAt: post.PublishAt.Format("02 Jan, 2006"),
386- PublishAtISO: post.PublishAt.Format(time.RFC3339),
387- Username: username,
388- BlogName: blogName,
389- Contents: template.HTML(text),
390- ImgURL: template.URL(cfg.ImgURL(curl, username, post.Slug)),
391- Tags: tagLinks,
392- }
393- } else {
394- data = PostPageData{
395- Site: *cfg.GetSiteData(),
396- BlogURL: template.URL(cfg.FullBlogURL(curl, username)),
397- PageTitle: "Post not found",
398- Caption: "Post not found",
399- Title: "Post not found",
400- PublishAt: time.Now().Format("02 Jan, 2006"),
401- PublishAtISO: time.Now().Format(time.RFC3339),
402- Username: username,
403- BlogName: blogName,
404- }
405- logger.Infof("post not found %s/%s", username, slug)
406- }
407-
408- ts, err := shared.RenderTemplate(cfg, []string{
409- cfg.StaticPath("html/post.page.tmpl"),
410- })
411-
412- if err != nil {
413- http.Error(w, err.Error(), http.StatusInternalServerError)
414- }
415-
416- err = ts.Execute(w, data)
417- if err != nil {
418- logger.Error(err)
419- http.Error(w, err.Error(), http.StatusInternalServerError)
420- }
421-}
422-
423-func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
424- username := shared.GetUsernameFromRequest(r)
425+func rssHandler(w http.ResponseWriter, r *http.Request) {
426 dbpool := shared.GetDB(r)
427 logger := shared.GetLogger(r)
428 cfg := shared.GetCfg(r)
429
430- user, err := dbpool.FindUserForName(username)
431- if err != nil {
432- logger.Infof("rss feed not found: %s", username)
433- http.Error(w, "rss feed not found", http.StatusNotFound)
434- return
435- }
436-
437- tag := r.URL.Query().Get("tag")
438- var posts []*db.Post
439- var p *db.Paginate[*db.Post]
440- pager := &db.Pager{Num: 10, Page: 0}
441- if tag == "" {
442- p, err = dbpool.FindPostsForUser(pager, user.ID, cfg.Space)
443- } else {
444- p, err = dbpool.FindUserPostsByTag(pager, tag, user.ID, cfg.Space)
445- }
446- posts = p.Data
447-
448+ pager, err := dbpool.FindAllPosts(&db.Pager{Num: 25, Page: 0}, Space)
449 if err != nil {
450 logger.Error(err)
451 http.Error(w, err.Error(), http.StatusInternalServerError)
452@@ -458,34 +54,27 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
453 return
454 }
455
456- headerTxt := &HeaderTxt{
457- Title: GetBlogName(username),
458- }
459-
460- curl := shared.CreateURLFromRequest(cfg, r)
461-
462 feed := &feeds.Feed{
463- Title: headerTxt.Title,
464- Link: &feeds.Link{Href: cfg.FullBlogURL(curl, username)},
465- Description: headerTxt.Bio,
466- Author: &feeds.Author{Name: username},
467+ Title: fmt.Sprintf("%s imgs feed", cfg.Domain),
468+ Link: &feeds.Link{Href: cfg.HomeURL()},
469+ Description: fmt.Sprintf("%s latest image", cfg.Domain),
470+ Author: &feeds.Author{Name: cfg.Domain},
471 Created: time.Now(),
472 }
473
474+ curl := shared.CreateURLFromRequest(cfg, r)
475+
476 var feedItems []*feeds.Item
477- for _, post := range posts {
478- if slices.Contains(cfg.HiddenPosts, post.Filename) {
479- continue
480- }
481+ for _, post := range pager.Data {
482 var tpl bytes.Buffer
483 data := &PostPageData{
484- ImgURL: template.URL(cfg.ImgURL(curl, username, post.Slug)),
485+ ImgURL: template.URL(cfg.ImgURL(curl, post.Username, post.Filename)),
486 }
487 if err := ts.Execute(&tpl, data); err != nil {
488 continue
489 }
490
491- realUrl := cfg.FullPostURL(curl, post.Username, post.Slug)
492+ realUrl := cfg.FullPostURL(curl, post.Username, post.Filename)
493 if !curl.Subdomain && !curl.UsernameInRoute {
494 realUrl = fmt.Sprintf("%s://%s%s", cfg.Protocol, r.Host, realUrl)
495 }
496@@ -494,10 +83,11 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
497 Id: realUrl,
498 Title: post.Title,
499 Link: &feeds.Link{Href: realUrl},
500- Created: *post.PublishAt,
501 Content: tpl.String(),
502+ Created: *post.PublishAt,
503 Updated: *post.UpdatedAt,
504 Description: post.Description,
505+ Author: &feeds.Author{Name: post.Username},
506 }
507
508 if post.Description != "" {
509@@ -521,98 +111,68 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
510 }
511 }
512
513-func rssHandler(w http.ResponseWriter, r *http.Request) {
514+func ImgRequest(w http.ResponseWriter, r *http.Request) {
515+ subdomain := shared.GetSubdomain(r)
516+ cfg := shared.GetCfg(r)
517 dbpool := shared.GetDB(r)
518 logger := shared.GetLogger(r)
519- cfg := shared.GetCfg(r)
520+ username := shared.GetUsernameFromRequest(r)
521
522- pager, err := dbpool.FindAllPosts(&db.Pager{Num: 25, Page: 0}, cfg.Space)
523+ user, err := dbpool.FindUserForName(username)
524 if err != nil {
525- logger.Error(err)
526- http.Error(w, err.Error(), http.StatusInternalServerError)
527+ logger.Infof("rss feed not found: %s", username)
528+ http.Error(w, "rss feed not found", http.StatusNotFound)
529 return
530 }
531
532- ts, err := template.ParseFiles(cfg.StaticPath("html/rss.page.tmpl"))
533- if err != nil {
534- logger.Error(err)
535- http.Error(w, err.Error(), http.StatusInternalServerError)
536- return
537+ var dimes string
538+ var slug string
539+ if !cfg.IsSubdomains() || subdomain == "" {
540+ slug, _ = url.PathUnescape(shared.GetField(r, 1))
541+ dimes, _ = url.PathUnescape(shared.GetField(r, 2))
542+ } else {
543+ slug, _ = url.PathUnescape(shared.GetField(r, 0))
544+ dimes, _ = url.PathUnescape(shared.GetField(r, 1))
545 }
546
547- feed := &feeds.Feed{
548- Title: fmt.Sprintf("%s imgs feed", cfg.Domain),
549- Link: &feeds.Link{Href: cfg.ReadURL()},
550- Description: fmt.Sprintf("%s latest image", cfg.Domain),
551- Author: &feeds.Author{Name: cfg.Domain},
552- Created: time.Now(),
553+ ratio, _ := storage.GetRatio(dimes)
554+ opts := &storage.ImgProcessOpts{
555+ Quality: 80,
556+ Ratio: ratio,
557 }
558
559- curl := shared.CreateURLFromRequest(cfg, r)
560-
561- var feedItems []*feeds.Item
562- for _, post := range pager.Data {
563- var tpl bytes.Buffer
564- data := &PostPageData{
565- ImgURL: template.URL(cfg.ImgURL(curl, post.Username, post.Slug)),
566- }
567- if err := ts.Execute(&tpl, data); err != nil {
568- continue
569- }
570-
571- realUrl := cfg.FullPostURL(curl, post.Username, post.Slug)
572- if !curl.Subdomain && !curl.UsernameInRoute {
573- realUrl = fmt.Sprintf("%s://%s%s", cfg.Protocol, r.Host, realUrl)
574- }
575-
576- item := &feeds.Item{
577- Id: realUrl,
578- Title: post.Title,
579- Link: &feeds.Link{Href: realUrl},
580- Content: tpl.String(),
581- Created: *post.PublishAt,
582- Updated: *post.UpdatedAt,
583- Description: post.Description,
584- Author: &feeds.Author{Name: post.Username},
585- }
586-
587- if post.Description != "" {
588- item.Description = post.Description
589+ ext := filepath.Ext(slug)
590+ // Files can contain periods. `filepath.Ext` is greedy and will clip the last period in the slug
591+ // and call that a file extension so we want to be explicit about what
592+ // file extensions we clip here
593+ for _, fext := range cfg.AllowedExt {
594+ if ext == fext {
595+ // users might add the file extension when requesting an image
596+ // but we want to remove that
597+ slug = shared.SanitizeFileExt(slug)
598+ break
599 }
600-
601- feedItems = append(feedItems, item)
602 }
603- feed.Items = feedItems
604
605- rss, err := feed.ToAtom()
606+ post, err := FindImgPost(r, user, slug)
607 if err != nil {
608- logger.Fatal(err)
609- http.Error(w, "Could not generate atom rss feed", http.StatusInternalServerError)
610+ errMsg := fmt.Sprintf("image not found %s/%s", user.Name, slug)
611+ logger.Infof(errMsg)
612+ http.Error(w, errMsg, http.StatusNotFound)
613+ return
614 }
615
616- w.Header().Add("Content-Type", "application/atom+xml")
617- _, err = w.Write([]byte(rss))
618- if err != nil {
619- logger.Error(err)
620- }
621+ fname := post.Filename
622+ pgs.ServeAsset(fname, opts, true, w, r)
623 }
624
625-func createStaticRoutes() []shared.Route {
626- return []shared.Route{
627- shared.NewRoute("GET", "/main.css", shared.ServeFile("main.css", "text/css")),
628- shared.NewRoute("GET", "/imgs.css", shared.ServeFile("imgs.css", "text/css")),
629- shared.NewRoute("GET", "/card.png", shared.ServeFile("card.png", "image/png")),
630- shared.NewRoute("GET", "/favicon-16x16.png", shared.ServeFile("favicon-16x16.png", "image/png")),
631- shared.NewRoute("GET", "/favicon-32x32.png", shared.ServeFile("favicon-32x32.png", "image/png")),
632- shared.NewRoute("GET", "/apple-touch-icon.png", shared.ServeFile("apple-touch-icon.png", "image/png")),
633- shared.NewRoute("GET", "/favicon.ico", shared.ServeFile("favicon.ico", "image/x-icon")),
634- shared.NewRoute("GET", "/robots.txt", shared.ServeFile("robots.txt", "text/plain")),
635- }
636+func FindImgPost(r *http.Request, user *db.User, slug string) (*db.Post, error) {
637+ dbpool := shared.GetDB(r)
638+ return dbpool.FindPostWithSlug(slug, user.ID, Space)
639 }
640
641 func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
642 routes := []shared.Route{
643- shared.NewRoute("GET", "/", shared.CreatePageHandler("html/marketing.page.tmpl")),
644 shared.NewRoute("GET", "/check", shared.CheckHandler),
645 }
646
647@@ -628,28 +188,16 @@ func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
648 shared.NewRoute("GET", "/atom.xml", rssHandler),
649 shared.NewRoute("GET", "/feed.xml", rssHandler),
650
651- shared.NewRoute("GET", "/([^/]+)", blogHandler),
652- shared.NewRoute("GET", "/([^/]+)/rss", rssBlogHandler),
653- shared.NewRoute("GET", "/([^/]+)/rss.xml", rssBlogHandler),
654- shared.NewRoute("GET", "/([^/]+)/atom.xml", rssBlogHandler),
655- shared.NewRoute("GET", "/([^/]+)/feed.xml", rssBlogHandler),
656- shared.NewRoute("GET", "/([^/]+)/o/([^/]+)", imgRequestOriginal),
657- shared.NewRoute("GET", "/([^/]+)/p/([^/]+)", postHandler),
658- shared.NewRoute("GET", "/([^/]+)/([^/]+)", imgRequest),
659- shared.NewRoute("GET", "/([^/]+)/([^/]+)/([a-z0-9]+)", imgRequest),
660+ shared.NewRoute("GET", "/([^/]+)/o/([^/]+)", ImgRequest),
661+ shared.NewRoute("GET", "/([^/]+)/([^/]+)", ImgRequest),
662+ shared.NewRoute("GET", "/([^/]+)/([^/]+)/([a-z0-9]+)", ImgRequest),
663 )
664
665 return routes
666 }
667
668 func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
669- routes := []shared.Route{
670- shared.NewRoute("GET", "/", blogHandler),
671- shared.NewRoute("GET", "/rss", rssBlogHandler),
672- shared.NewRoute("GET", "/rss.xml", rssBlogHandler),
673- shared.NewRoute("GET", "/atom.xml", rssBlogHandler),
674- shared.NewRoute("GET", "/feed.xml", rssBlogHandler),
675- }
676+ routes := []shared.Route{}
677
678 routes = append(
679 routes,
680@@ -658,10 +206,9 @@ func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
681
682 routes = append(
683 routes,
684- shared.NewRoute("GET", "/o/([^/]+)", imgRequestOriginal),
685- shared.NewRoute("GET", "/p/([^/]+)", postHandler),
686- shared.NewRoute("GET", "/([^/]+)", imgRequest),
687- shared.NewRoute("GET", "/([^/]+)/([a-z0-9]+)", imgRequest),
688+ shared.NewRoute("GET", "/o/([^/]+)", ImgRequest),
689+ shared.NewRoute("GET", "/([^/]+)", ImgRequest),
690+ shared.NewRoute("GET", "/([^/]+)/([a-z0-9]+)", ImgRequest),
691 )
692
693 return routes
694@@ -691,8 +238,7 @@ func StartApiServer() {
695 logger.Fatal(err)
696 }
697
698- staticRoutes := createStaticRoutes()
699-
700+ staticRoutes := []shared.Route{}
701 if cfg.Debug {
702 staticRoutes = shared.CreatePProfRoutes(staticRoutes)
703 }
+0,
-10
1@@ -1,10 +0,0 @@
2-package imgs
3-
4-import (
5- "github.com/picosh/send/send/utils"
6-)
7-
8-type IImgsAPI interface {
9- HasAccess(userID string) bool
10- Upload(file *utils.FileEntry) (string, error)
11-}
+0,
-24
1@@ -5,28 +5,6 @@ import (
2 "github.com/picosh/pico/wish/cms/config"
3 )
4
5-type ImgsLinkify struct {
6- Cfg *shared.ConfigSite
7- Username string
8- OnSubdomain bool
9- WithUsername bool
10-}
11-
12-func NewImgsLinkify(username string) *ImgsLinkify {
13- cfg := NewConfigSite()
14- return &ImgsLinkify{
15- Cfg: cfg,
16- Username: username,
17- }
18-}
19-
20-func (i *ImgsLinkify) Create(fname string) string {
21- return i.Cfg.ImgFullURL(i.Username, fname)
22-}
23-
24-var maxSize = uint64(500 * shared.MB)
25-var maxImgSize = int64(10 * shared.MB)
26-
27 func NewConfigSite() *shared.ConfigSite {
28 debug := shared.GetEnv("IMGS_DEBUG", "0")
29 domain := shared.GetEnv("IMGS_DOMAIN", "prose.sh")
30@@ -67,8 +45,6 @@ func NewConfigSite() *shared.ConfigSite {
31 AllowedExt: []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"},
32 Logger: shared.CreateLogger(debug == "1"),
33 AllowRegister: allowRegister == "1",
34- MaxSize: maxSize,
35- MaxAssetSize: maxImgSize,
36 },
37 }
38
+0,
-20
1@@ -1,20 +0,0 @@
2-{{define "base"}}
3-<!doctype html>
4-<html lang="en">
5- <head>
6- <meta charset='utf-8'>
7- <meta name="viewport" content="width=device-width, initial-scale=1" />
8- <title>{{template "title" .}}</title>
9-
10- <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
11-
12- <meta name="keywords" content="image, images, hosting" />
13-
14- <link rel="stylesheet" href="https://pico.sh/smol.css" />
15- <link rel="stylesheet" href="/main.css" />
16-
17- {{template "meta" .}}
18- </head>
19- <body {{template "attrs" .}}>{{template "body" .}}</body>
20-</html>
21-{{end}}
+0,
-66
1@@ -1,66 +0,0 @@
2-{{template "base" .}}
3-
4-{{define "title"}}{{.PageTitle}}{{end}}
5-
6-{{define "meta"}}
7-<meta name="description" content="{{if .Header.Bio}}{{.Header.Bio}}{{else}}{{.Header.Title}}{{end}}" />
8-
9-<meta property="og:type" content="website">
10-<meta property="og:site_name" content="{{.Site.Domain}}">
11-<meta property="og:url" content="{{.URL}}">
12-<meta property="og:title" content="{{.Header.Title}}">
13-{{if .Header.Bio}}<meta property="og:description" content="{{.Header.Bio}}">{{end}}
14-<meta property="og:image:width" content="300" />
15-<meta property="og:image:height" content="300" />
16-<meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
17-<meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
18-
19-<meta property="twitter:card" content="summary">
20-<meta property="twitter:url" content="{{.URL}}">
21-<meta property="twitter:title" content="{{.Header.Title}}">
22-{{if .Header.Bio}}<meta property="twitter:description" content="{{.Header.Bio}}">{{end}}
23-<meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
24-<meta name="twitter:image:src" content="https://{{.Site.Domain}}/card.png" />
25-
26-<link rel="alternate" href="{{.RSSURL}}" type="application/rss+xml" title="RSS feed for {{.Header.Title}}" />
27-{{end}}
28-
29-{{define "attrs"}}id="blog"{{end}}
30-
31-{{define "body"}}
32-<header class="text-center">
33- <h1 class="text-2xl font-bold">{{.Header.Title}}</h1>
34- {{if .Header.Bio}}<p class="text-lg">{{.Header.Bio}}</p>{{end}}
35- <nav>
36- {{range .Header.Nav}}
37- <a href="{{.URL}}" class="text-lg">{{.Text}}</a> |
38- {{end}}
39- <a href="{{.RSSURL}}" class="text-lg">rss</a>
40- </nav>
41- <hr />
42-</header>
43-<main>
44- {{if .Readme.HasText}}
45- <section>
46- <article class="md">
47- {{.Readme.Contents}}
48- </article>
49- <hr />
50- </section>
51- {{end}}
52-
53- {{if .HasFilter}}
54- <a href={{.URL}}>clear filters</a>
55- {{end}}
56- <section class="albums">
57- {{range .Posts}}
58- <article class="thumbnail-container">
59- <a href="{{.URL}}" class="thumbnail-link">
60- <img class="thumbnail" src="{{.ImgURL}}" alt="{{.Caption}}" />
61- </a>
62- </article>
63- {{end}}
64- </section>
65-</main>
66-{{template "footer" .}}
67-{{end}}
1@@ -1,6 +0,0 @@
2-{{define "footer"}}
3-<footer>
4- <hr />
5- published with <a href={{.Site.HomeURL}}>{{.Site.Domain}}</a>
6-</footer>
7-{{end}}
1@@ -1,9 +0,0 @@
2-{{define "marketing-footer"}}
3-<footer>
4- <hr />
5- <p class="font-italic">Built and maintained by <a href="https://pico.sh">pico.sh</a>.</p>
6- <div>
7- <a href="/rss">rss</a>
8- </div>
9-</footer>
10-{{end}}
+0,
-39
1@@ -1,39 +0,0 @@
2-{{template "base" .}}
3-
4-{{define "title"}}{{.Site.Domain}} -- image hosting for hackers{{end}}
5-
6-{{define "meta"}}
7-<meta name="description" content="image hosting for hackers" />
8-
9-<meta property="og:type" content="website">
10-<meta property="og:site_name" content="{{.Site.Domain}}">
11-<meta property="og:url" content="https://{{.Site.Domain}}">
12-<meta property="og:title" content="{{.Site.Domain}}">
13-<meta property="og:description" content="image hosting for hackers">
14-
15-<meta name="twitter:card" content="summary" />
16-<meta property="twitter:url" content="https://{{.Site.Domain}}">
17-<meta property="twitter:title" content="{{.Site.Domain}}">
18-<meta property="twitter:description" content="image hosting for hackers">
19-<meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
20-<meta name="twitter:image:src" content="https://{{.Site.Domain}}/card.png" />
21-
22-<meta property="og:image:width" content="300" />
23-<meta property="og:image:height" content="300" />
24-<meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
25-<meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
26-{{end}}
27-
28-{{define "attrs"}}{{end}}
29-
30-{{define "body"}}
31-<header class="text-center">
32- <h1 class="text-2xl font-bold">{{.Site.Domain}}</h1>
33- <p class="text-lg">image hosting for hackers</p>
34- <div>
35- <a href="https://pico.sh/getting-started" class="btn-link mt inline-block">GET STARTED</a>
36- </div>
37-</header>
38-
39-{{template "marketing-footer" .}}
40-{{end}}
+0,
-70
1@@ -1,70 +0,0 @@
2-{{template "base" .}}
3-
4-{{define "title"}}{{.PageTitle}}{{end}}
5-
6-{{define "meta"}}
7-<meta name="description" content="{{.Caption}}" />
8-
9-<meta property="og:type" content="website">
10-<meta property="og:site_name" content="{{.Site.Domain}}">
11-<meta property="og:url" content="{{.URL}}">
12-<meta property="og:title" content="{{.PageTitle}}">
13-{{if .Caption}}<meta property="og:description" content="{{.Caption}}">{{end}}
14-<meta property="og:image:width" content="300" />
15-<meta property="og:image:height" content="300" />
16-<meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
17-<meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
18-
19-<meta property="twitter:card" content="summary">
20-<meta property="twitter:url" content="{{.URL}}">
21-<meta property="twitter:title" content="{{.PageTitle}}">
22-{{if .Caption}}<meta property="twitter:description" content="{{.Caption}}">{{end}}
23-<meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
24-<meta name="twitter:image:src" content="https://{{.Site.Domain}}/card.png" />
25-{{end}}
26-
27-{{define "attrs"}}id="post" class="{{.Slug}}"{{end}}
28-
29-{{define "body"}}
30-<header>
31- {{if .Title}}<h1 class="text-2xl font-bold">{{.Title}}</h1>{{end}}
32- <p class="font-bold m-0">
33- <time datetime="{{.PublishAtISO}}">{{.PublishAt}}</time>
34- </p>
35- <div class="tags">
36- <a href="{{.BlogURL}}">< {{.BlogName}}</a>
37- {{range .Tags}}
38- <a class="tag" href="{{.URL}}">#{{.Text}}</a>
39- {{end}}
40- </div>
41-</header>
42-<main>
43- <article>
44- <figure class="img">
45- <a href="{{.ImgURL}}">
46- <img src="{{.ImgURL}}" alt="" />
47- </a>
48- {{if .Caption}}<figcaption class="my font-italic">{{.Caption}}</figcaption>{{end}}
49- </figure>
50-
51- <div class="md">{{.Contents}}</div>
52-
53- {{if .ImgURL}}
54- <dl>
55- <dt>Hotlink</dt>
56- <dd><a href="{{.ImgURL}}">{{.ImgURL}}</a></dd>
57-
58- <dt>Resize width (preserve aspect)</dt>
59- <dd><a href="{{.ImgURL}}/300x">{{.ImgURL}}/300x</a></dd>
60-
61- <dt>Resize height (preserve aspect)</dt>
62- <dd><a href="{{.ImgURL}}/x300">{{.ImgURL}}/x300</a></dd>
63-
64- <dt>Resize width and height</dt>
65- <dd><a href="{{.ImgURL}}/300x300">{{.ImgURL}}/300x300</a></dd>
66- </dl>
67- {{end}}
68- </article>
69-</main>
70-{{template "footer" .}}
71-{{end}}
+0,
-0
+0,
-0
+0,
-0
+0,
-0
+0,
-0
+0,
-84
1@@ -1,84 +0,0 @@
2-body {
3- max-width: 52rem;
4-}
5-
6-img {
7- max-width: 100%;
8- max-height: 90vh;
9-}
10-
11-.albums {
12- width: 100%;
13- display: grid;
14- grid-template-columns: repeat(3, 1fr);
15- grid-template-rows: repeat(auto-fill, 300px);
16- grid-row-gap: 0.5rem;
17- grid-column-gap: 1rem;
18-}
19-
20-.thumbnail-container {
21- position: relative;
22-}
23-
24-.thumbnail-container a, .thumbnail-container a:visited, .thumbnail-container a:hover {
25- color: var(--white);
26-}
27-
28-.tag-text {
29- position: absolute;
30- bottom: 20px;
31- z-index: 1;
32- text-align: center;
33- width: 100%;
34-}
35-
36-.thumbnail {
37- z-index: 1;
38- object-fit: contain;
39- width: 300px;
40- height: 300px;
41- background-color: #000;
42-}
43-
44-.thumbnail-link {
45- z-index: 1;
46-}
47-
48-.md h1 {
49- font-size: 1.85rem;
50- line-height: 1.15;
51- font-weight: bold;
52- padding: 0.6rem 0 0 0;
53-}
54-
55-.md h2 {
56- font-size: 1.45rem;
57- line-height: 1.15;
58- font-weight: bold;
59- padding: 0.6rem 0 0 0;
60-}
61-
62-.md h3 {
63- font-size: 1.25rem;
64- font-weight: bold;
65- padding: 0.6rem 0 0 0;
66-}
67-
68-.md h4 {
69- font-size: 1rem;
70- font-weight: bold;
71- padding: 0.6rem 0 0 0;
72-}
73-
74-@media only screen and (max-width: 900px) {
75- .albums {
76- grid-template-columns: repeat(1, 1fr);
77- justify-content: center;
78- }
79-
80- .albums article {
81- display: flex;
82- flex-direction: column;
83- align-items: center;
84- }
85-}
+0,
-2
1@@ -1,2 +0,0 @@
2-User-agent: *
3-Allow: /
+8,
-15
1@@ -16,7 +16,6 @@ import (
2 gocache "github.com/patrickmn/go-cache"
3 "github.com/picosh/pico/db"
4 "github.com/picosh/pico/db/postgres"
5- "github.com/picosh/pico/imgs"
6 "github.com/picosh/pico/shared"
7 "github.com/picosh/pico/shared/storage"
8 )
9@@ -161,8 +160,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
10 }
11 header, err := dbpool.FindPostWithFilename("_header.txt", user.ID, cfg.Space)
12 if err == nil {
13- linkify := imgs.NewImgsLinkify(username)
14- parsedText := shared.ListParseText(header.Text, linkify)
15+ parsedText := shared.ListParseText(header.Text)
16 if parsedText.Title != "" {
17 headerTxt.Title = parsedText.Title
18 }
19@@ -184,8 +182,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
20 readmeTxt := &ReadmeTxt{}
21 readme, err := dbpool.FindPostWithFilename("_readme.txt", user.ID, cfg.Space)
22 if err == nil {
23- linkify := imgs.NewImgsLinkify(username)
24- parsedText := shared.ListParseText(readme.Text, linkify)
25+ parsedText := shared.ListParseText(readme.Text)
26 readmeTxt.Items = parsedText.Items
27 readmeTxt.ListType = parsedText.ListType
28 if len(readmeTxt.Items) > 0 {
29@@ -300,9 +297,8 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
30
31 header, _ := dbpool.FindPostWithFilename("_header.txt", user.ID, cfg.Space)
32 blogName := GetBlogName(username)
33- linkify := imgs.NewImgsLinkify(username)
34 if header != nil {
35- headerParsed := shared.ListParseText(header.Text, linkify)
36+ headerParsed := shared.ListParseText(header.Text)
37 if headerParsed.Title != "" {
38 blogName = headerParsed.Title
39 }
40@@ -311,12 +307,12 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
41 var data PostPageData
42 post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
43 if err == nil {
44- parsedText := shared.ListParseText(post.Text, linkify)
45+ parsedText := shared.ListParseText(post.Text)
46
47 // we need the blog name from the readme unfortunately
48 readme, err := dbpool.FindPostWithFilename("_readme.txt", user.ID, cfg.Space)
49 if err == nil {
50- readmeParsed := shared.ListParseText(readme.Text, linkify)
51+ readmeParsed := shared.ListParseText(readme.Text)
52 if readmeParsed.Title != "" {
53 blogName = readmeParsed.Title
54 }
55@@ -498,8 +494,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
56 }
57 header, err := dbpool.FindPostWithFilename("_header.txt", user.ID, cfg.Space)
58 if err == nil {
59- linkify := imgs.NewImgsLinkify(username)
60- parsedText := shared.ListParseText(header.Text, linkify)
61+ parsedText := shared.ListParseText(header.Text)
62 if parsedText.Title != "" {
63 headerTxt.Title = parsedText.Title
64 }
65@@ -522,8 +517,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
66 if slices.Contains(cfg.HiddenPosts, post.Filename) {
67 continue
68 }
69- linkify := imgs.NewImgsLinkify(username)
70- parsed := shared.ListParseText(post.Text, linkify)
71+ parsed := shared.ListParseText(post.Text)
72 var tpl bytes.Buffer
73 data := &PostPageData{
74 ListType: parsed.ListType,
75@@ -597,8 +591,7 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
76
77 var feedItems []*feeds.Item
78 for _, post := range pager.Data {
79- linkify := imgs.NewImgsLinkify(post.Username)
80- parsed := shared.ListParseText(post.Text, linkify)
81+ parsed := shared.ListParseText(post.Text)
82 var tpl bytes.Buffer
83 data := &PostPageData{
84 ListType: parsed.ListType,
+1,
-3
1@@ -9,7 +9,6 @@ import (
2 "github.com/charmbracelet/ssh"
3 "github.com/picosh/pico/db"
4 "github.com/picosh/pico/filehandlers"
5- "github.com/picosh/pico/imgs"
6 "github.com/picosh/pico/shared"
7 )
8
9@@ -41,8 +40,7 @@ func (p *ListHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaData)
10 }
11
12 func (p *ListHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData) error {
13- linkify := imgs.NewImgsLinkify(data.Username)
14- parsedText := shared.ListParseText(string(data.Text), linkify)
15+ parsedText := shared.ListParseText(string(data.Text))
16
17 if parsedText.Title == "" {
18 data.Title = shared.ToUpper(data.Slug)
+6,
-3
1@@ -33,7 +33,7 @@ func (me *SSHServer) authHandler(ctx ssh.Context, key ssh.PublicKey) bool {
2 return true
3 }
4
5-func createRouter(handler *filehandlers.ScpUploadHandler) proxy.Router {
6+func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
7 return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
8 return []wish.Middleware{
9 pipe.Middleware(handler, ".txt"),
10@@ -47,7 +47,7 @@ func createRouter(handler *filehandlers.ScpUploadHandler) proxy.Router {
11 }
12 }
13
14-func withProxy(handler *filehandlers.ScpUploadHandler, otherMiddleware ...wish.Middleware) ssh.Option {
15+func withProxy(handler *filehandlers.FileHandlerRouter, otherMiddleware ...wish.Middleware) ssh.Option {
16 return func(server *ssh.Server) error {
17 err := sftp.SSHOption(handler)(server)
18 if err != nil {
19@@ -84,7 +84,10 @@ func StartSshServer() {
20 logger.Fatal(err)
21 }
22
23- handler := filehandlers.NewScpPostHandler(dbh, cfg, hooks, st)
24+ fileMap := map[string]filehandlers.ReadWriteHandler{
25+ "fallback": filehandlers.NewScpPostHandler(dbh, cfg, hooks, st),
26+ }
27+ handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
28
29 sshServer := &SSHServer{}
30 s, err := wish.NewServer(
R pastes/cms.go =>
pastes/ssh.go
+6,
-3
1@@ -33,7 +33,7 @@ func (me *SSHServer) authHandler(ctx ssh.Context, key ssh.PublicKey) bool {
2 return true
3 }
4
5-func createRouter(handler *filehandlers.ScpUploadHandler) proxy.Router {
6+func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
7 return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
8 return []wish.Middleware{
9 pipe.Middleware(handler, ""),
10@@ -47,7 +47,7 @@ func createRouter(handler *filehandlers.ScpUploadHandler) proxy.Router {
11 }
12 }
13
14-func withProxy(handler *filehandlers.ScpUploadHandler, otherMiddleware ...wish.Middleware) ssh.Option {
15+func withProxy(handler *filehandlers.FileHandlerRouter, otherMiddleware ...wish.Middleware) ssh.Option {
16 return func(server *ssh.Server) error {
17 err := sftp.SSHOption(handler)(server)
18 if err != nil {
19@@ -83,7 +83,10 @@ func StartSshServer() {
20 logger.Fatal(err)
21 }
22
23- handler := filehandlers.NewScpPostHandler(dbh, cfg, hooks, st)
24+ fileMap := map[string]filehandlers.ReadWriteHandler{
25+ "fallback": filehandlers.NewScpPostHandler(dbh, cfg, hooks, st),
26+ }
27+ handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
28
29 sshServer := &SSHServer{}
30 s, err := wish.NewServer(
+72,
-45
1@@ -24,16 +24,18 @@ import (
2 )
3
4 type AssetHandler struct {
5- Username string
6- Subdomain string
7- Filepath string
8- ProjectDir string
9- Cfg *shared.ConfigSite
10- Dbpool db.DB
11- Storage storage.ObjectStorage
12- Logger *zap.SugaredLogger
13- Cache *gocache.Cache
14- UserID string
15+ Username string
16+ Subdomain string
17+ Filepath string
18+ ProjectDir string
19+ Cfg *shared.ConfigSite
20+ Dbpool db.DB
21+ Storage storage.ObjectStorage
22+ Logger *zap.SugaredLogger
23+ Cache *gocache.Cache
24+ UserID string
25+ Bucket storage.Bucket
26+ ImgProcessOpts *storage.ImgProcessOpts
27 }
28
29 func checkHandler(w http.ResponseWriter, r *http.Request) {
30@@ -216,16 +218,9 @@ func calcPossibleRoutes(projectName, fp string, userRedirects []*RedirectRule) [
31 return rts
32 }
33
34-func assetHandler(w http.ResponseWriter, h *AssetHandler) {
35- bucket, err := h.Storage.GetBucket(shared.GetAssetBucketName(h.UserID))
36- if err != nil {
37- h.Logger.Infof("bucket not found for %s", h.Username)
38- http.Error(w, "bucket not found", http.StatusNotFound)
39- return
40- }
41-
42+func (h *AssetHandler) handle(w http.ResponseWriter) {
43 var redirects []*RedirectRule
44- redirectFp, _, _, err := h.Storage.GetFile(bucket, filepath.Join(h.ProjectDir, "_redirects"))
45+ redirectFp, _, _, err := h.Storage.GetFile(h.Bucket, filepath.Join(h.ProjectDir, "_redirects"))
46 if err == nil {
47 defer redirectFp.Close()
48 buf := new(strings.Builder)
49@@ -243,13 +238,24 @@ func assetHandler(w http.ResponseWriter, h *AssetHandler) {
50 }
51
52 routes := calcPossibleRoutes(h.ProjectDir, h.Filepath, redirects)
53- var contents utils.ReaderAtCloser
54+ var contents io.ReadCloser
55 assetFilepath := ""
56 status := 200
57 attempts := []string{}
58 for _, fp := range routes {
59 attempts = append(attempts, fp.Filepath)
60- c, _, _, err := h.Storage.GetFile(bucket, fp.Filepath)
61+ mimeType := storage.GetMimeType(fp.Filepath)
62+ var c io.ReadCloser
63+ var err error
64+ if strings.HasPrefix(mimeType, "image/") {
65+ c, _, err = h.Storage.ServeFile(
66+ h.Bucket,
67+ fp.Filepath,
68+ h.ImgProcessOpts,
69+ )
70+ } else {
71+ c, _, _, err = h.Storage.GetFile(h.Bucket, fp.Filepath)
72+ }
73 if err == nil {
74 contents = c
75 assetFilepath = fp.Filepath
76@@ -261,7 +267,7 @@ func assetHandler(w http.ResponseWriter, h *AssetHandler) {
77 if assetFilepath == "" {
78 h.Logger.Infof(
79 "asset not found in bucket: bucket:[%s], routes:[%s]",
80- bucket.Name,
81+ h.Bucket.Name,
82 strings.Join(attempts, ", "),
83 )
84 http.Error(w, "404 not found", http.StatusNotFound)
85@@ -298,14 +304,14 @@ func getProjectFromSubdomain(subdomain string) (*SubdomainProps, error) {
86 return props, nil
87 }
88
89-func serveAsset(subdomain string, w http.ResponseWriter, r *http.Request) {
90+func ServeAsset(fname string, opts *storage.ImgProcessOpts, fromImgs bool, w http.ResponseWriter, r *http.Request) {
91+ subdomain := shared.GetSubdomain(r)
92 cfg := shared.GetCfg(r)
93 dbpool := shared.GetDB(r)
94 st := shared.GetStorage(r)
95 logger := shared.GetLogger(r)
96 cache := shared.GetCache(r)
97
98- floc, _ := url.PathUnescape(shared.GetField(r, 0))
99 props, err := getProjectFromSubdomain(subdomain)
100 if err != nil {
101 logger.Info(err)
102@@ -319,29 +325,50 @@ func serveAsset(subdomain string, w http.ResponseWriter, r *http.Request) {
103 http.Error(w, "user not found", http.StatusNotFound)
104 return
105 }
106- projectDir := props.ProjectName
107- project, err := dbpool.FindProjectByName(user.ID, props.ProjectName)
108- if err == nil {
109- projectDir = project.ProjectDir
110+
111+ // TODO: this could probably be cleaned up more
112+ // imgs wont have a project directory
113+ projectDir := ""
114+ var bucket storage.Bucket
115+ // imgs has a different bucket directory
116+ if fromImgs {
117+ bucket, err = st.GetBucket(shared.GetImgsBucketName(user.ID))
118+ } else {
119+ bucket, err = st.GetBucket(shared.GetAssetBucketName(user.ID))
120+ projectDir = props.ProjectName
121+ project, err := dbpool.FindProjectByName(user.ID, props.ProjectName)
122+ if err == nil {
123+ projectDir = project.ProjectDir
124+ }
125 }
126
127- assetHandler(w, &AssetHandler{
128- Username: props.Username,
129- UserID: user.ID,
130- Subdomain: subdomain,
131- ProjectDir: projectDir,
132- Filepath: floc,
133- Cfg: cfg,
134- Dbpool: dbpool,
135- Storage: st,
136- Logger: logger,
137- Cache: cache,
138- })
139+ if err != nil {
140+ logger.Infof("bucket not found for %s", props.Username)
141+ http.Error(w, "bucket not found", http.StatusNotFound)
142+ return
143+ }
144+
145+ asset := &AssetHandler{
146+ Username: props.Username,
147+ UserID: user.ID,
148+ Subdomain: subdomain,
149+ ProjectDir: projectDir,
150+ Filepath: fname,
151+ Cfg: cfg,
152+ Dbpool: dbpool,
153+ Storage: st,
154+ Logger: logger,
155+ Cache: cache,
156+ Bucket: bucket,
157+ ImgProcessOpts: opts,
158+ }
159+
160+ asset.handle(w)
161 }
162
163-func assetRequest(w http.ResponseWriter, r *http.Request) {
164- subdomain := shared.GetSubdomain(r)
165- serveAsset(subdomain, w, r)
166+func AssetRequest(w http.ResponseWriter, r *http.Request) {
167+ fname, _ := url.PathUnescape(shared.GetField(r, 0))
168+ ServeAsset(fname, nil, false, w, r)
169 }
170
171 func StartApiServer() {
172@@ -382,8 +409,8 @@ func StartApiServer() {
173 shared.NewRoute("GET", "/(.+)", shared.CreatePageHandler("html/marketing.page.tmpl")),
174 }
175 subdomainRoutes := []shared.Route{
176- shared.NewRoute("GET", "/", assetRequest),
177- shared.NewRoute("GET", "/(.+)", assetRequest),
178+ shared.NewRoute("GET", "/", AssetRequest),
179+ shared.NewRoute("GET", "/(.+)", AssetRequest),
180 }
181
182 handler := shared.CreateServe(mainRoutes, subdomainRoutes, cfg, db, st, logger, cache)
+20,
-13
1@@ -205,8 +205,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
2
3 readme, err := dbpool.FindPostWithFilename("_readme.md", user.ID, cfg.Space)
4 if err == nil {
5- linkify := imgs.NewImgsLinkify(readme.Username)
6- parsedText, err := shared.ParseText(readme.Text, linkify)
7+ parsedText, err := shared.ParseText(readme.Text)
8 if err != nil {
9 logger.Error(err)
10 }
11@@ -354,9 +353,8 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
12 hasCSS := false
13 var data PostPageData
14 post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
15- linkify := imgs.NewImgsLinkify(username)
16 if err == nil {
17- parsedText, err := shared.ParseText(post.Text, linkify)
18+ parsedText, err := shared.ParseText(post.Text)
19 if err != nil {
20 logger.Error(err)
21 }
22@@ -364,7 +362,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
23 // we need the blog name from the readme unfortunately
24 readme, err := dbpool.FindPostWithFilename("_readme.md", user.ID, cfg.Space)
25 if err == nil {
26- readmeParsed, err := shared.ParseText(readme.Text, linkify)
27+ readmeParsed, err := shared.ParseText(readme.Text)
28 if err != nil {
29 logger.Error(err)
30 }
31@@ -394,7 +392,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
32 footer, err := dbpool.FindPostWithFilename("_footer.md", user.ID, cfg.Space)
33 var footerHTML template.HTML
34 if err == nil {
35- footerParsed, err := shared.ParseText(footer.Text, linkify)
36+ footerParsed, err := shared.ParseText(footer.Text)
37 if err != nil {
38 logger.Error(err)
39 }
40@@ -437,6 +435,14 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
41 Unlisted: unlisted,
42 }
43 } else {
44+ // TODO: HACK to support imgs slugs inside prose
45+ // We definitely want to kill this feature in time
46+ imgPost, err := imgs.FindImgPost(r, user, slug)
47+ if err == nil && imgPost != nil {
48+ imgs.ImgRequest(w, r)
49+ return
50+ }
51+
52 data = PostPageData{
53 Site: *cfg.GetSiteData(),
54 BlogURL: template.URL(cfg.FullBlogURL(curl, username)),
55@@ -593,8 +599,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
56
57 readme, err := dbpool.FindPostWithFilename("_readme.md", user.ID, cfg.Space)
58 if err == nil {
59- linkify := imgs.NewImgsLinkify(readme.Username)
60- parsedText, err := shared.ParseText(readme.Text, linkify)
61+ parsedText, err := shared.ParseText(readme.Text)
62 if err != nil {
63 logger.Error(err)
64 }
65@@ -624,8 +629,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
66 if slices.Contains(cfg.HiddenPosts, post.Filename) {
67 continue
68 }
69- linkify := imgs.NewImgsLinkify(post.Username)
70- parsed, err := shared.ParseText(post.Text, linkify)
71+ parsed, err := shared.ParseText(post.Text)
72 if err != nil {
73 logger.Error(err)
74 }
75@@ -633,7 +637,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
76 footer, err := dbpool.FindPostWithFilename("_footer.md", user.ID, cfg.Space)
77 var footerHTML string
78 if err == nil {
79- footerParsed, err := shared.ParseText(footer.Text, linkify)
80+ footerParsed, err := shared.ParseText(footer.Text)
81 if err != nil {
82 logger.Error(err)
83 }
84@@ -711,8 +715,7 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
85
86 var feedItems []*feeds.Item
87 for _, post := range pager.Data {
88- linkify := imgs.NewImgsLinkify(post.Username)
89- parsed, err := shared.ParseText(post.Text, linkify)
90+ parsed, err := shared.ParseText(post.Text)
91 if err != nil {
92 logger.Error(err)
93 }
94@@ -824,6 +827,8 @@ func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
95 shared.NewRoute("GET", "/([^/]+)/feed.xml", rssBlogHandler),
96 shared.NewRoute("GET", "/([^/]+)/_styles.css", blogStyleHandler),
97 shared.NewRoute("GET", "/raw/([^/]+)/(.+)", postRawHandler),
98+ shared.NewRoute("GET", "/([^/]+)/(.+)/([a-z0-9]+)", imgs.ImgRequest),
99+ shared.NewRoute("GET", "/([^/]+)/(.+).(jpg|jpeg|png|gif|webp|svg)", imgs.ImgRequest),
100 shared.NewRoute("GET", "/([^/]+)/(.+)", postHandler),
101 )
102
103@@ -850,6 +855,8 @@ func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
104 routes = append(
105 routes,
106 shared.NewRoute("GET", "/raw/(.+)", postRawHandler),
107+ shared.NewRoute("GET", "/(.+)/([a-z0-9]+)", imgs.ImgRequest),
108+ shared.NewRoute("GET", "/(.+).(jpg|jpeg|png|gif|webp|svg)", imgs.ImgRequest),
109 shared.NewRoute("GET", "/(.+)", postHandler),
110 )
111
+25,
-13
1@@ -20,6 +20,8 @@ func NewConfigSite() *shared.ConfigSite {
2 minioPass := shared.GetEnv("MINIO_ROOT_PASSWORD", "")
3 dbURL := shared.GetEnv("DATABASE_URL", "")
4 useImgProxy := shared.GetEnv("USE_IMGPROXY", "1")
5+ maxSize := uint64(500 * shared.MB)
6+ maxImgSize := int64(10 * shared.MB)
7
8 intro := "To get started, enter a username.\n"
9 intro += "To learn next steps go to our docs at https://pico.sh/prose\n"
10@@ -30,22 +32,32 @@ func NewConfigSite() *shared.ConfigSite {
11 CustomdomainsEnabled: customdomains == "1",
12 UseImgProxy: useImgProxy == "1",
13 ConfigCms: config.ConfigCms{
14- Domain: domain,
15- Email: email,
16- Port: port,
17- Protocol: protocol,
18- DbURL: dbURL,
19- StorageDir: storageDir,
20- MinioURL: minioURL,
21- MinioUser: minioUser,
22- MinioPass: minioPass,
23- Description: "A blog platform for hackers.",
24- IntroText: intro,
25- Space: "prose",
26- AllowedExt: []string{".md"},
27+ Domain: domain,
28+ Email: email,
29+ Port: port,
30+ Protocol: protocol,
31+ DbURL: dbURL,
32+ StorageDir: storageDir,
33+ MinioURL: minioURL,
34+ MinioUser: minioUser,
35+ MinioPass: minioPass,
36+ Description: "A blog platform for hackers.",
37+ IntroText: intro,
38+ Space: "prose",
39+ AllowedExt: []string{
40+ ".md",
41+ ".jpg",
42+ ".jpeg",
43+ ".png",
44+ ".gif",
45+ ".webp",
46+ ".svg",
47+ },
48 HiddenPosts: []string{"_readme.md", "_styles.css", "_footer.md"},
49 Logger: shared.CreateLogger(debug == "1"),
50 AllowRegister: allowRegister == "1",
51+ MaxSize: maxSize,
52+ MaxAssetSize: maxImgSize,
53 },
54 }
55 }
+1,
-3
1@@ -9,7 +9,6 @@ import (
2 "github.com/charmbracelet/ssh"
3 "github.com/picosh/pico/db"
4 "github.com/picosh/pico/filehandlers"
5- "github.com/picosh/pico/imgs"
6 "github.com/picosh/pico/shared"
7 )
8
9@@ -48,8 +47,7 @@ func (p *MarkdownHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaD
10 }
11
12 func (p *MarkdownHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData) error {
13- linkify := imgs.NewImgsLinkify("")
14- parsedText, err := shared.ParseText(data.Text, linkify)
15+ parsedText, err := shared.ParseText(data.Text)
16 // we return nil here because we don't want the file upload to fail
17 if err != nil {
18 return nil
+9,
-3
1@@ -15,6 +15,7 @@ import (
2 lm "github.com/charmbracelet/wish/logging"
3 "github.com/picosh/pico/db/postgres"
4 "github.com/picosh/pico/filehandlers"
5+ uploadimgs "github.com/picosh/pico/filehandlers/imgs"
6 "github.com/picosh/pico/shared"
7 "github.com/picosh/pico/shared/storage"
8 "github.com/picosh/pico/wish/cms"
9@@ -33,7 +34,7 @@ func (me *SSHServer) authHandler(ctx ssh.Context, key ssh.PublicKey) bool {
10 return true
11 }
12
13-func createRouter(handler *filehandlers.ScpUploadHandler) proxy.Router {
14+func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
15 return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
16 return []wish.Middleware{
17 pipe.Middleware(handler, ".md"),
18@@ -47,7 +48,7 @@ func createRouter(handler *filehandlers.ScpUploadHandler) proxy.Router {
19 }
20 }
21
22-func withProxy(handler *filehandlers.ScpUploadHandler, otherMiddleware ...wish.Middleware) ssh.Option {
23+func withProxy(handler *filehandlers.FileHandlerRouter, otherMiddleware ...wish.Middleware) ssh.Option {
24 return func(server *ssh.Server) error {
25 err := sftp.SSHOption(handler)(server)
26 if err != nil {
27@@ -83,7 +84,12 @@ func StartSshServer() {
28 logger.Fatal(err)
29 }
30
31- handler := filehandlers.NewScpPostHandler(dbh, cfg, hooks, st)
32+ fileMap := map[string]filehandlers.ReadWriteHandler{
33+ ".md": filehandlers.NewScpPostHandler(dbh, cfg, hooks, st),
34+ "fallback": uploadimgs.NewUploadImgHandler(dbh, cfg, st),
35+ }
36+ handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
37+ handler.Spaces = []string{cfg.Space, "imgs"}
38
39 sshServer := &SSHServer{}
40 s, err := wish.NewServer(
1@@ -9,6 +9,10 @@ import (
2 "github.com/picosh/send/send/utils"
3 )
4
5+func GetImgsBucketName(userID string) string {
6+ return userID
7+}
8+
9 func GetAssetBucketName(userID string) string {
10 return fmt.Sprintf("static-%s", userID)
11 }
1@@ -1,15 +0,0 @@
2-package shared
3-
4-type Linkify interface {
5- Create(fname string) string
6-}
7-
8-type NullLinkify struct{}
9-
10-func (n *NullLinkify) Create(s string) string {
11- return ""
12-}
13-
14-func NewNullLinkify() *NullLinkify {
15- return &NullLinkify{}
16-}
1@@ -150,7 +150,7 @@ func KeyAsValue(token *SplitToken) string {
2 return token.Value
3 }
4
5-func parseItem(meta *ListMetaData, li *ListItem, prevItem *ListItem, pre bool, mod int, linkify Linkify) (bool, bool, int) {
6+func parseItem(meta *ListMetaData, li *ListItem, prevItem *ListItem, pre bool, mod int) (bool, bool, int) {
7 skip := false
8
9 if strings.HasPrefix(li.Value, preToken) {
10@@ -178,13 +178,6 @@ func parseItem(meta *ListMetaData, li *ListItem, prevItem *ListItem, pre bool, m
11 li.IsImg = true
12 split := TextToSplitToken(strings.Replace(li.Value, imgToken, "", 1))
13 key := split.Key
14- if strings.HasPrefix(key, "/") {
15- frag := SanitizeFileExt(key)
16- key = linkify.Create(frag)
17- } else if strings.HasPrefix(key, "./") {
18- name := SanitizeFileExt(key[1:])
19- key = linkify.Create(name)
20- }
21 li.URL = template.URL(key)
22 li.Value = KeyAsValue(split)
23 } else if strings.HasPrefix(li.Value, varToken) {
24@@ -204,7 +197,7 @@ func parseItem(meta *ListMetaData, li *ListItem, prevItem *ListItem, pre bool, m
25 old := len(li.Value)
26 li.Value = trim
27
28- pre, skip, _ = parseItem(meta, li, prevItem, pre, mod, linkify)
29+ pre, skip, _ = parseItem(meta, li, prevItem, pre, mod)
30 if prevItem != nil && prevItem.Indent == 0 {
31 mod = old - len(trim)
32 li.Indent = 1
33@@ -223,7 +216,7 @@ func parseItem(meta *ListMetaData, li *ListItem, prevItem *ListItem, pre bool, m
34 return pre, skip, mod
35 }
36
37-func ListParseText(text string, linkify Linkify) *ListParsedText {
38+func ListParseText(text string) *ListParsedText {
39 textItems := SplitByNewline(text)
40 items := []*ListItem{}
41 meta := ListMetaData{
42@@ -245,7 +238,7 @@ func ListParseText(text string, linkify Linkify) *ListParsedText {
43 Value: t,
44 }
45
46- pre, skip, mod = parseItem(&meta, &li, prevItem, pre, mod, linkify)
47+ pre, skip, mod = parseItem(&meta, &li, prevItem, pre, mod)
48
49 if li.IsText && li.Value == "" {
50 skip = true
1@@ -12,12 +12,9 @@ import (
2 "github.com/yuin/goldmark"
3 highlighting "github.com/yuin/goldmark-highlighting"
4 meta "github.com/yuin/goldmark-meta"
5- "github.com/yuin/goldmark/ast"
6 "github.com/yuin/goldmark/extension"
7 "github.com/yuin/goldmark/parser"
8- "github.com/yuin/goldmark/renderer"
9 ghtml "github.com/yuin/goldmark/renderer/html"
10- "github.com/yuin/goldmark/util"
11 "go.abhg.dev/goldmark/anchor"
12 yaml "gopkg.in/yaml.v2"
13 )
14@@ -160,67 +157,7 @@ func toTags(obj interface{}) ([]string, error) {
15 return arr, nil
16 }
17
18-type ImgRender struct {
19- ghtml.Config
20- ImgURL func(url []byte) []byte
21-}
22-
23-func NewImgsRenderer(url func([]byte) []byte) renderer.NodeRenderer {
24- return &ImgRender{
25- Config: ghtml.NewConfig(),
26- ImgURL: url,
27- }
28-}
29-
30-func (r *ImgRender) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
31- reg.Register(ast.KindImage, r.renderImage)
32-}
33-
34-func (r *ImgRender) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
35- if !entering {
36- return ast.WalkContinue, nil
37- }
38- n := node.(*ast.Image)
39- _, _ = w.WriteString("<img src=\"")
40- if r.Unsafe || !ghtml.IsDangerousURL(n.Destination) {
41- dest := r.ImgURL(n.Destination)
42- _, _ = w.Write(util.EscapeHTML(util.URLEscape(dest, true)))
43- }
44- _, _ = w.WriteString(`" alt="`)
45- _, _ = w.Write(util.EscapeHTML(n.Text(source)))
46- _ = w.WriteByte('"')
47- if n.Title != nil {
48- _, _ = w.WriteString(` title="`)
49- r.Writer.Write(w, n.Title)
50- _ = w.WriteByte('"')
51- }
52- if n.Attributes() != nil {
53- ghtml.RenderAttributes(w, n, ghtml.ImageAttributeFilter)
54- }
55- if r.XHTML {
56- _, _ = w.WriteString(" />")
57- } else {
58- _, _ = w.WriteString(">")
59- }
60- return ast.WalkSkipChildren, nil
61-}
62-
63-func CreateImgURL(linkify Linkify) func([]byte) []byte {
64- return func(url []byte) []byte {
65- if url[0] == '/' {
66- name := SanitizeFileExt(string(url))
67- nextURL := linkify.Create(name)
68- return []byte(nextURL)
69- } else if bytes.HasPrefix(url, []byte{'.', '/'}) {
70- name := SanitizeFileExt(string(url[1:]))
71- nextURL := linkify.Create(name)
72- return []byte(nextURL)
73- }
74- return url
75- }
76-}
77-
78-func ParseText(text string, linkify Linkify) (*ParsedText, error) {
79+func ParseText(text string) (*ParsedText, error) {
80 parsed := ParsedText{
81 MetaData: &MetaData{
82 Tags: []string{},
83@@ -250,9 +187,6 @@ func ParseText(text string, linkify Linkify) (*ParsedText, error) {
84 ),
85 goldmark.WithRendererOptions(
86 ghtml.WithUnsafe(),
87- renderer.WithNodeRenderers(
88- util.Prioritized(NewImgsRenderer(CreateImgURL(linkify)), 0),
89- ),
90 ),
91 )
92 context := parser.NewContext()
93@@ -266,21 +200,9 @@ func ParseText(text string, linkify Linkify) (*ParsedText, error) {
94 parsed.MetaData.Description = toString(metaData["description"])
95 parsed.MetaData.Layout = toString(metaData["layout"])
96 parsed.MetaData.Image = toString(metaData["image"])
97- if strings.HasPrefix(parsed.Image, "/") {
98- parsed.Image = linkify.Create(parsed.Image)
99- } else if strings.HasPrefix(parsed.Image, "./") {
100- parsed.Image = linkify.Create(parsed.Image[1:])
101- }
102-
103 parsed.MetaData.ImageCard = toString(metaData["card"])
104 parsed.MetaData.Hidden = toBool(metaData["draft"])
105-
106 parsed.MetaData.Favicon = toString(metaData["favicon"])
107- if strings.HasPrefix(parsed.Favicon, "/") {
108- parsed.Favicon = linkify.Create(parsed.Favicon)
109- } else if strings.HasPrefix(parsed.Favicon, "./") {
110- parsed.Favicon = linkify.Create(parsed.Favicon[1:])
111- }
112
113 var publishAt *time.Time = nil
114 var err error
1@@ -107,8 +107,8 @@ func (s *StorageFS) GetFile(bucket Bucket, fpath string) (utils.ReaderAtCloser,
2 return dat, info.Size(), info.ModTime(), nil
3 }
4
5-func (s *StorageFS) ServeFile(bucket Bucket, fpath string, ratio *Ratio, original bool, useProxy bool) (io.ReadCloser, string, error) {
6- if !useProxy || original || os.Getenv("IMGPROXY_URL") == "" {
7+func (s *StorageFS) ServeFile(bucket Bucket, fpath string, opts *ImgProcessOpts) (io.ReadCloser, string, error) {
8+ if opts == nil || os.Getenv("IMGPROXY_URL") == "" {
9 contentType := GetMimeType(fpath)
10 rc, _, _, err := s.GetFile(bucket, fpath)
11 return rc, contentType, err
12@@ -116,8 +116,7 @@ func (s *StorageFS) ServeFile(bucket Bucket, fpath string, ratio *Ratio, origina
13
14 filePath := filepath.Join(bucket.Path, fpath)
15 dataURL := fmt.Sprintf("local://%s", filePath)
16-
17- return HandleProxy(dataURL, ratio, original, useProxy)
18+ return HandleProxy(dataURL, opts)
19 }
20
21 func (s *StorageFS) PutFile(bucket Bucket, fpath string, contents utils.ReaderAtCloser, entry *utils.FileEntry) (string, error) {
1@@ -183,8 +183,8 @@ func (s *StorageMinio) GetFile(bucket Bucket, fpath string) (utils.ReaderAtClose
2 return obj, info.Size, modTime, nil
3 }
4
5-func (s *StorageMinio) ServeFile(bucket Bucket, fpath string, ratio *Ratio, original bool, useProxy bool) (io.ReadCloser, string, error) {
6- if !useProxy || original || os.Getenv("IMGPROXY_URL") == "" {
7+func (s *StorageMinio) ServeFile(bucket Bucket, fpath string, opts *ImgProcessOpts) (io.ReadCloser, string, error) {
8+ if opts == nil || os.Getenv("IMGPROXY_URL") == "" {
9 contentType := GetMimeType(fpath)
10 rc, _, _, err := s.GetFile(bucket, fpath)
11 return rc, contentType, err
12@@ -192,8 +192,7 @@ func (s *StorageMinio) ServeFile(bucket Bucket, fpath string, ratio *Ratio, orig
13
14 filePath := filepath.Join(bucket.Name, fpath)
15 dataURL := fmt.Sprintf("s3://%s", filePath)
16-
17- return HandleProxy(dataURL, ratio, original, useProxy)
18+ return HandleProxy(dataURL, opts)
19 }
20
21 func (s *StorageMinio) PutFile(bucket Bucket, fpath string, contents utils.ReaderAtCloser, entry *utils.FileEntry) (string, error) {
1@@ -61,25 +61,41 @@ func GetMimeType(fpath string) string {
2 return "text/plain"
3 }
4
5-func HandleProxy(dataURL string, ratio *Ratio, original bool, useProxy bool) (io.ReadCloser, string, error) {
6+type ImgProcessOpts struct {
7+ Quality int
8+ Ratio *Ratio
9+}
10+
11+func (img *ImgProcessOpts) String() string {
12+ processOpts := ""
13+
14+ if img.Quality != 0 {
15+ processOpts = fmt.Sprintf("%s/q:%d", processOpts, img.Quality)
16+ }
17+
18+ if img.Ratio != nil {
19+ processOpts = fmt.Sprintf(
20+ "%s/s:%d:%d",
21+ processOpts,
22+ img.Ratio.Width,
23+ img.Ratio.Height,
24+ )
25+ }
26+
27+ return processOpts
28+}
29+
30+func HandleProxy(dataURL string, opts *ImgProcessOpts) (io.ReadCloser, string, error) {
31 imgProxyURL := os.Getenv("IMGPROXY_URL")
32 imgProxySalt := os.Getenv("IMGPROXY_SALT")
33 imgProxyKey := os.Getenv("IMGPROXY_KEY")
34
35 signature := "_"
36- processOpts := "q:80"
37
38- if ratio != nil {
39- processOpts += fmt.Sprintf("/s:%d:%d", ratio.Width, ratio.Height)
40- }
41+ processOpts := opts.String()
42
43- fileType := ".webp"
44- if original {
45- fileType = ""
46- processOpts = "raw:1"
47- }
48-
49- processPath := fmt.Sprintf("/%s/%s%s", processOpts, base64.StdEncoding.EncodeToString([]byte(dataURL)), fileType)
50+ fileType := ""
51+ processPath := fmt.Sprintf("%s/%s%s", processOpts, base64.StdEncoding.EncodeToString([]byte(dataURL)), fileType)
52
53 if imgProxySalt != "" && imgProxyKey != "" {
54 keyBin, err := hex.DecodeString(imgProxyKey)
55@@ -105,5 +121,9 @@ func HandleProxy(dataURL string, ratio *Ratio, original bool, useProxy bool) (io
56 return nil, "", err
57 }
58
59+ if res.StatusCode < 200 || res.StatusCode >= 300 {
60+ return nil, "", fmt.Errorf("%s", res.Status)
61+ }
62+
63 return res.Body, res.Header.Get("Content-Type"), nil
64 }
1@@ -23,7 +23,7 @@ type ObjectStorage interface {
2 GetBucketQuota(bucket Bucket) (uint64, error)
3 GetFileSize(bucket Bucket, fpath string) (int64, error)
4 GetFile(bucket Bucket, fpath string) (utils.ReaderAtCloser, int64, time.Time, error)
5- ServeFile(bucket Bucket, fpath string, ratio *Ratio, original bool, useProxy bool) (io.ReadCloser, string, error)
6+ ServeFile(bucket Bucket, fpath string, opts *ImgProcessOpts) (io.ReadCloser, string, error)
7 PutFile(bucket Bucket, fpath string, contents utils.ReaderAtCloser, entry *utils.FileEntry) (string, error)
8 DeleteFile(bucket Bucket, fpath string) error
9 ListFiles(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error)