- commit
- ba016c3
- parent
- 9798290
- author
- Eric Bower
- date
- 2022-08-19 02:44:40 +0000 UTC
feat(prose): allow images to be uploaded from other services
16 files changed,
+266,
-115
+7,
-2
1@@ -9,8 +9,9 @@ import (
2 "time"
3
4 "git.sr.ht/~erock/pico/db/postgres"
5+ "git.sr.ht/~erock/pico/filehandlers/imgs"
6 "git.sr.ht/~erock/pico/imgs"
7- "git.sr.ht/~erock/pico/imgs/upload"
8+ "git.sr.ht/~erock/pico/imgs/storage"
9 "git.sr.ht/~erock/pico/shared"
10 "git.sr.ht/~erock/pico/wish/cms"
11 "git.sr.ht/~erock/pico/wish/pipe"
12@@ -72,7 +73,11 @@ func main() {
13 logger := cfg.Logger
14 dbh := postgres.NewDB(&cfg.ConfigCms)
15 defer dbh.Close()
16- handler := upload.NewUploadImgHandler(dbh, cfg, imgs.NewStorageFS(cfg.StorageDir))
17+ handler := uploadimgs.NewUploadImgHandler(
18+ dbh,
19+ cfg,
20+ storage.NewStorageFS(cfg.StorageDir),
21+ )
22
23 sshServer := &SSHServer{}
24 s, err := wish.NewServer(
+1,
-1
1@@ -97,7 +97,7 @@ func main() {
2 logger.Info("updating dates")
3 for _, post := range posts {
4 if post.Space == "prose" {
5- parsed, err := shared.ParseText(post.Text)
6+ parsed, err := shared.ParseText(post.Text, "")
7 if err != nil {
8 logger.Error(err)
9 continue
1@@ -75,7 +75,7 @@ func main() {
2
3 logger.Info("replacing tags")
4 for _, post := range posts {
5- parsed, err := shared.ParseText(post.Text)
6+ parsed, err := shared.ParseText(post.Text, "")
7 if err != nil {
8 continue
9 }
R imgs/upload/handler.go =>
filehandlers/imgs/handler.go
+16,
-7
1@@ -1,4 +1,4 @@
2-package upload
3+package uploadimgs
4
5 import (
6 "encoding/binary"
7@@ -9,8 +9,7 @@ import (
8 "time"
9
10 "git.sr.ht/~erock/pico/db"
11- "git.sr.ht/~erock/pico/filehandlers"
12- "git.sr.ht/~erock/pico/imgs"
13+ "git.sr.ht/~erock/pico/imgs/storage"
14 "git.sr.ht/~erock/pico/shared"
15 "git.sr.ht/~erock/pico/wish/cms/util"
16 "git.sr.ht/~erock/pico/wish/send/utils"
17@@ -21,14 +20,23 @@ var GB = 1024 * 1024 * 1024
18 var maxSize = 2 * GB
19 var mdMime = "text/markdown; charset=UTF-8"
20
21+type PostMetaData struct {
22+ *db.Post
23+ OrigText []byte
24+ Cur *db.Post
25+ Tags []string
26+ User *db.User
27+ FileEntry *utils.FileEntry
28+}
29+
30 type UploadImgHandler struct {
31 User *db.User
32 DBPool db.DB
33 Cfg *shared.ConfigSite
34- Storage *imgs.StorageFS
35+ Storage *storage.StorageFS
36 }
37
38-func NewUploadImgHandler(dbpool db.DB, cfg *shared.ConfigSite, storage *imgs.StorageFS) *UploadImgHandler {
39+func NewUploadImgHandler(dbpool db.DB, cfg *shared.ConfigSite, storage *storage.StorageFS) *UploadImgHandler {
40 return &UploadImgHandler{
41 DBPool: dbpool,
42 Cfg: cfg,
43@@ -36,7 +44,7 @@ func NewUploadImgHandler(dbpool db.DB, cfg *shared.ConfigSite, storage *imgs.Sto
44 }
45 }
46
47-func (h *UploadImgHandler) removePost(data *filehandlers.PostMetaData) error {
48+func (h *UploadImgHandler) removePost(data *PostMetaData) error {
49 // skip empty files from being added to db
50 if data.Post == nil {
51 h.Cfg.Logger.Infof("(%s) is empty, skipping record", data.Filename)
52@@ -112,7 +120,8 @@ func (h *UploadImgHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
53 h.Cfg.Logger.Info(err)
54 }
55
56- metadata := filehandlers.PostMetaData{
57+ metadata := PostMetaData{
58+ OrigText: text,
59 Post: &nextPost,
60 User: h.User,
61 FileEntry: entry,
R imgs/upload/img.go =>
filehandlers/imgs/img.go
+7,
-8
1@@ -1,15 +1,14 @@
2-package upload
3+package uploadimgs
4
5 import (
6 "fmt"
7 "strings"
8
9 "git.sr.ht/~erock/pico/db"
10- "git.sr.ht/~erock/pico/filehandlers"
11 "git.sr.ht/~erock/pico/shared"
12 )
13
14-func (h *UploadImgHandler) validateImg(data *filehandlers.PostMetaData) (bool, error) {
15+func (h *UploadImgHandler) validateImg(data *PostMetaData) (bool, error) {
16 if !h.DBPool.HasFeatureForUser(data.User.ID, "imgs") {
17 return false, fmt.Errorf("ERROR: user (%s) does not have access to this feature (imgs)", data.User.Name)
18 }
19@@ -35,7 +34,7 @@ func (h *UploadImgHandler) validateImg(data *filehandlers.PostMetaData) (bool, e
20 return true, nil
21 }
22
23-func (h *UploadImgHandler) metaImg(data *filehandlers.PostMetaData) error {
24+func (h *UploadImgHandler) metaImg(data *PostMetaData) error {
25 // create or get
26 bucket, err := h.Storage.UpsertBucket(data.User.ID)
27 if err != nil {
28@@ -60,7 +59,7 @@ func (h *UploadImgHandler) metaImg(data *filehandlers.PostMetaData) error {
29 return nil
30 }
31
32-func (h *UploadImgHandler) writeImg(data *filehandlers.PostMetaData) error {
33+func (h *UploadImgHandler) writeImg(data *PostMetaData) error {
34 valid, err := h.validateImg(data)
35 if !valid {
36 return err
37@@ -72,7 +71,7 @@ func (h *UploadImgHandler) writeImg(data *filehandlers.PostMetaData) error {
38 return err
39 }
40
41- if len(data.Text) == 0 {
42+ if len(data.OrigText) == 0 {
43 err = h.removePost(data)
44 if err != nil {
45 return err
46@@ -107,8 +106,8 @@ func (h *UploadImgHandler) writeImg(data *filehandlers.PostMetaData) error {
47 return fmt.Errorf("error for %s: %v", data.Filename, err)
48 }
49 } else {
50- if shared.Shasum([]byte(data.Text)) == data.Cur.Shasum {
51- h.Cfg.Logger.Infof("(%s) found, but text is identical, skipping", data.Filename)
52+ if shared.Shasum(data.OrigText) == data.Cur.Shasum {
53+ h.Cfg.Logger.Infof("(%s) found, but image is identical, skipping", data.Filename)
54 return nil
55 }
56
R imgs/upload/md.go =>
filehandlers/imgs/md.go
+25,
-9
1@@ -1,16 +1,14 @@
2-package upload
3+package uploadimgs
4
5 import (
6 "fmt"
7 "strings"
8
9 "git.sr.ht/~erock/pico/db"
10- "git.sr.ht/~erock/pico/filehandlers"
11- "git.sr.ht/~erock/pico/prose"
12 "git.sr.ht/~erock/pico/shared"
13 )
14
15-func (h *UploadImgHandler) validateMd(data *filehandlers.PostMetaData) (bool, error) {
16+func (h *UploadImgHandler) validateMd(data *PostMetaData) (bool, error) {
17 if !shared.IsTextFile(data.Text) {
18 err := fmt.Errorf(
19 "WARNING: (%s) invalid file must be plain text (utf-8), skipping",
20@@ -30,11 +28,24 @@ func (h *UploadImgHandler) validateMd(data *filehandlers.PostMetaData) (bool, er
21 return true, nil
22 }
23
24-func (h *UploadImgHandler) metaMd(data *filehandlers.PostMetaData) error {
25- hooks := prose.MarkdownHooks{Cfg: h.Cfg}
26- err := hooks.FileMeta(data)
27+func (h *UploadImgHandler) metaMd(data *PostMetaData) error {
28+ parsedText, err := shared.ParseText(data.Text, "")
29+ // we return nil here because we don't want the file upload to fail
30 if err != nil {
31- return err
32+ return nil
33+ }
34+
35+ if parsedText.Title == "" {
36+ data.Title = shared.ToUpper(data.Slug)
37+ } else {
38+ data.Title = parsedText.Title
39+ }
40+
41+ data.Tags = parsedText.Tags
42+ data.Description = parsedText.Description
43+
44+ if parsedText.PublishAt != nil && !parsedText.PublishAt.IsZero() {
45+ data.PublishAt = parsedText.MetaData.PublishAt
46 }
47
48 if data.Cur != nil {
49@@ -53,7 +64,7 @@ func (h *UploadImgHandler) metaMd(data *filehandlers.PostMetaData) error {
50 return nil
51 }
52
53-func (h *UploadImgHandler) writeMd(data *filehandlers.PostMetaData) error {
54+func (h *UploadImgHandler) writeMd(data *PostMetaData) error {
55 valid, err := h.validateMd(data)
56 if !valid {
57 return err
58@@ -105,6 +116,11 @@ func (h *UploadImgHandler) writeMd(data *filehandlers.PostMetaData) error {
59 }
60 }
61 } else {
62+ if data.Text == data.Cur.Text {
63+ h.Cfg.Logger.Infof("(%s) found, but metadata is identical, skipping", data.Filename)
64+ return nil
65+ }
66+
67 h.Cfg.Logger.Infof("(%s) found, updating record", data.Filename)
68 updatePost := db.Post{
69 ID: data.Cur.ID,
+19,
-12
1@@ -10,6 +10,7 @@ import (
2 "time"
3
4 "git.sr.ht/~erock/pico/db"
5+ "git.sr.ht/~erock/pico/imgs"
6 "git.sr.ht/~erock/pico/shared"
7 "git.sr.ht/~erock/pico/wish/cms/util"
8 "git.sr.ht/~erock/pico/wish/send/utils"
9@@ -69,9 +70,14 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
10 userID := h.User.ID
11 filename := entry.Name
12
13- user, err := h.DBPool.FindUser(userID)
14- if err != nil {
15- return "", fmt.Errorf("error for %s: %v", filename, err)
16+ client := imgs.NewImgsAPI(h.DBPool)
17+ if shared.IsExtAllowed(filename, client.Cfg.AllowedExt) {
18+ if !client.HasAccess(userID) {
19+ msg := "user (%s) does not have access to imgs.sh, cannot upload file (%s)"
20+ return "", fmt.Errorf(msg, h.User.Name, filename)
21+ }
22+
23+ return client.Upload(s, entry)
24 }
25
26 var text []byte
27@@ -79,6 +85,13 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
28 text = b
29 }
30
31+ mimeType := http.DetectContentType(text)
32+ ext := path.Ext(filename)
33+ // DetectContentType does not detect markdown
34+ if ext == ".md" {
35+ mimeType = "text/markdown; charset=UTF-8"
36+ }
37+
38 now := time.Now()
39 slug := shared.SanitizeFileExt(filename)
40 fileSize := binary.Size(text)
41@@ -89,17 +102,11 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
42 Slug: slug,
43 PublishAt: &now,
44 Text: string(text),
45- MimeType: http.DetectContentType(text),
46+ MimeType: mimeType,
47 FileSize: fileSize,
48 Shasum: shasum,
49 }
50
51- ext := path.Ext(filename)
52- // DetectContentType does not detect markdown
53- if ext == ".md" {
54- nextPost.MimeType = "text/markdown; charset=UTF-8"
55- }
56-
57 metadata := PostMetaData{
58 Post: &nextPost,
59 User: h.User,
60@@ -181,7 +188,7 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
61 } else {
62 if metadata.Text == post.Text {
63 logger.Infof("(%s) found, but text is identical, skipping", filename)
64- return h.Cfg.FullPostURL(user.Name, metadata.Slug, h.Cfg.IsSubdomains(), true), nil
65+ return h.Cfg.FullPostURL(h.User.Name, metadata.Slug, h.Cfg.IsSubdomains(), true), nil
66 }
67
68 logger.Infof("(%s) found, updating record", filename)
69@@ -214,5 +221,5 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
70 }
71 }
72
73- return h.Cfg.FullPostURL(user.Name, metadata.Slug, h.Cfg.IsSubdomains(), true), nil
74+ return h.Cfg.FullPostURL(h.User.Name, metadata.Slug, h.Cfg.IsSubdomains(), true), nil
75 }
+19,
-59
1@@ -12,6 +12,7 @@ import (
2
3 "git.sr.ht/~erock/pico/db"
4 "git.sr.ht/~erock/pico/db/postgres"
5+ "git.sr.ht/~erock/pico/imgs/storage"
6 "git.sr.ht/~erock/pico/shared"
7 "github.com/gorilla/feeds"
8 "golang.org/x/exp/slices"
9@@ -112,45 +113,6 @@ type MergePost struct {
10
11 var allTag = "all"
12
13-func ImgURL(c *shared.ConfigSite, username string, slug string, onSubdomain bool, withUserName bool) string {
14- fname := url.PathEscape(strings.TrimLeft(slug, "/"))
15- if c.IsSubdomains() && onSubdomain {
16- return fmt.Sprintf("%s://%s.%s/%s", c.Protocol, username, c.Domain, fname)
17- }
18-
19- if withUserName {
20- return fmt.Sprintf("/%s/%s", username, fname)
21- }
22-
23- return fmt.Sprintf("/%s", fname)
24-}
25-
26-func TagURL(c *shared.ConfigSite, username, tag string, onSubdomain, withUserName bool) string {
27- tg := url.PathEscape(tag)
28- if c.IsSubdomains() && onSubdomain {
29- return fmt.Sprintf("%s://%s.%s/t/%s", c.Protocol, username, c.Domain, tg)
30- }
31-
32- if withUserName {
33- return fmt.Sprintf("/%s/t/%s", username, tg)
34- }
35-
36- return fmt.Sprintf("/t/%s", tg)
37-}
38-
39-func TagPostURL(c *shared.ConfigSite, username, tag, slug string, onSubdomain, withUserName bool) string {
40- fname := url.PathEscape(strings.TrimLeft(slug, "/"))
41- if c.IsSubdomains() && onSubdomain {
42- return fmt.Sprintf("%s://%s.%s/%s/%s", c.Protocol, username, c.Domain, tag, fname)
43- }
44-
45- if withUserName {
46- return fmt.Sprintf("/%s/%s/%s", username, tag, fname)
47- }
48-
49- return fmt.Sprintf("/%s/%s", tag, fname)
50-}
51-
52 func GetPostTitle(post *db.Post) string {
53 if post.Description == "" {
54 return post.Title
55@@ -238,8 +200,8 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
56 for key, post := range tagMap {
57 postCollection = append(postCollection, &PostTagData{
58 Tag: key,
59- URL: template.URL(TagURL(cfg, post.Username, key, onSubdomain, withUserName)),
60- ImgURL: template.URL(ImgURL(cfg, post.Username, post.Filename, onSubdomain, withUserName)),
61+ URL: template.URL(cfg.TagURL(post.Username, key, onSubdomain, withUserName)),
62+ ImgURL: template.URL(cfg.ImgURL(post.Username, post.Filename, onSubdomain, withUserName)),
63 PublishAt: post.PublishAt,
64 })
65 }
66@@ -303,14 +265,14 @@ func imgHandler(w http.ResponseWriter, r *http.Request) {
67 }
68 }
69
70- storage := NewStorageFS(cfg.StorageDir)
71- bucket, err := storage.GetBucket(user.ID)
72+ st := storage.NewStorageFS(cfg.StorageDir)
73+ bucket, err := st.GetBucket(user.ID)
74 if err != nil {
75 logger.Infof("bucket not found %s/%s", username, filename)
76 http.Error(w, err.Error(), http.StatusInternalServerError)
77 return
78 }
79- contents, err := storage.GetFile(bucket, post.Filename)
80+ contents, err := st.GetFile(bucket, post.Filename)
81 if err != nil {
82 logger.Infof("file not found %s/%s", username, post.Filename)
83 http.Error(w, err.Error(), http.StatusInternalServerError)
84@@ -369,8 +331,8 @@ func tagHandler(w http.ResponseWriter, r *http.Request) {
85 continue
86 }
87 mergedPosts = append(mergedPosts, TagPostData{
88- URL: template.URL(TagPostURL(cfg, username, tag, post.Slug, onSubdomain, withUserName)),
89- ImgURL: template.URL(ImgURL(cfg, username, post.Filename, onSubdomain, withUserName)),
90+ URL: template.URL(cfg.TagPostURL(username, tag, post.Slug, onSubdomain, withUserName)),
91+ ImgURL: template.URL(cfg.ImgURL(username, post.Filename, onSubdomain, withUserName)),
92 Caption: post.Title,
93 })
94 }
95@@ -382,7 +344,7 @@ func tagHandler(w http.ResponseWriter, r *http.Request) {
96 Site: *cfg.GetSiteData(),
97 Tag: tag,
98 Posts: mergedPosts,
99- URL: template.URL(TagURL(cfg, username, tag, onSubdomain, withUserName)),
100+ URL: template.URL(cfg.TagURL(username, tag, onSubdomain, withUserName)),
101 }
102
103 ts, err := shared.RenderTemplate(cfg, []string{
104@@ -459,8 +421,7 @@ func tagPostHandler(w http.ResponseWriter, r *http.Request) {
105 }
106
107 if i+1 < len(mergedPosts) {
108- nextPage = TagPostURL(
109- cfg,
110+ nextPage = cfg.TagPostURL(
111 username,
112 tag,
113 mergedPosts[i+1].Slug,
114@@ -470,8 +431,7 @@ func tagPostHandler(w http.ResponseWriter, r *http.Request) {
115 }
116
117 if i-1 >= 0 {
118- prevPage = TagPostURL(
119- cfg,
120+ prevPage = cfg.TagPostURL(
121 username,
122 tag,
123 mergedPosts[i-1].Slug,
124@@ -488,7 +448,7 @@ func tagPostHandler(w http.ResponseWriter, r *http.Request) {
125 return
126 }
127
128- parsed, err := shared.ParseText(post.Text)
129+ parsed, err := shared.ParseText(post.Text, cfg.ImgURL(username, "", true, false))
130 if err != nil {
131 logger.Error(err)
132 }
133@@ -500,7 +460,7 @@ func tagPostHandler(w http.ResponseWriter, r *http.Request) {
134 tagLinks := make([]Link, 0, len(post.Tags))
135 for _, tag := range post.Tags {
136 tagLinks = append(tagLinks, Link{
137- URL: template.URL(TagURL(cfg, username, tag, onSubdomain, withUserName)),
138+ URL: template.URL(cfg.TagURL(username, tag, onSubdomain, withUserName)),
139 Text: tag,
140 })
141 }
142@@ -518,7 +478,7 @@ func tagPostHandler(w http.ResponseWriter, r *http.Request) {
143 Username: username,
144 BlogName: blogName,
145 Contents: template.HTML(text),
146- ImgURL: template.URL(ImgURL(cfg, username, post.Filename, onSubdomain, withUserName)),
147+ ImgURL: template.URL(cfg.ImgURL(username, post.Filename, onSubdomain, withUserName)),
148 Tags: tagLinks,
149 PrevPage: template.URL(prevPage),
150 NextPage: template.URL(nextPage),
151@@ -571,7 +531,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
152 var data PostPageData
153 post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
154 if err == nil {
155- parsed, err := shared.ParseText(post.Text)
156+ parsed, err := shared.ParseText(post.Text, cfg.ImgURL(username, "", true, false))
157 if err != nil {
158 logger.Error(err)
159 }
160@@ -583,7 +543,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
161 tagLinks := make([]Link, 0, len(post.Tags))
162 for _, tag := range post.Tags {
163 tagLinks = append(tagLinks, Link{
164- URL: template.URL(TagURL(cfg, username, tag, onSubdomain, withUserName)),
165+ URL: template.URL(cfg.TagURL(username, tag, onSubdomain, withUserName)),
166 Text: tag,
167 })
168 }
169@@ -601,7 +561,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
170 Username: username,
171 BlogName: blogName,
172 Contents: template.HTML(text),
173- ImgURL: template.URL(ImgURL(cfg, username, post.Filename, onSubdomain, withUserName)),
174+ ImgURL: template.URL(cfg.ImgURL(username, post.Filename, onSubdomain, withUserName)),
175 Tags: tagLinks,
176 }
177 } else {
178@@ -721,7 +681,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
179 }
180 var tpl bytes.Buffer
181 data := &PostPageData{
182- ImgURL: template.URL(ImgURL(cfg, username, post.Filename, onSubdomain, withUserName)),
183+ ImgURL: template.URL(cfg.ImgURL(username, post.Filename, onSubdomain, withUserName)),
184 }
185 if err := ts.Execute(&tpl, data); err != nil {
186 continue
187@@ -800,7 +760,7 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
188 for _, post := range pager.Data {
189 var tpl bytes.Buffer
190 data := &PostPageData{
191- ImgURL: template.URL(ImgURL(cfg, post.Username, post.Filename, onSubdomain, withUserName)),
192+ ImgURL: template.URL(cfg.ImgURL(post.Username, post.Filename, onSubdomain, withUserName)),
193 }
194 if err := ts.Execute(&tpl, data); err != nil {
195 continue
+42,
-0
1@@ -0,0 +1,42 @@
2+package imgs
3+
4+import (
5+ "git.sr.ht/~erock/pico/db"
6+ "git.sr.ht/~erock/pico/filehandlers/imgs"
7+ "git.sr.ht/~erock/pico/imgs/storage"
8+ "git.sr.ht/~erock/pico/shared"
9+ "git.sr.ht/~erock/pico/wish/send/utils"
10+ "github.com/gliderlabs/ssh"
11+)
12+
13+type IImgsAPI interface {
14+ HasAccess(userID string) bool
15+ Upload(file *utils.FileEntry) (string, error)
16+}
17+
18+type ImgsAPI struct {
19+ Cfg *shared.ConfigSite
20+ Db db.DB
21+}
22+
23+func NewImgsAPI(dbpool db.DB) *ImgsAPI {
24+ cfg := NewConfigSite()
25+ return &ImgsAPI{
26+ Cfg: cfg,
27+ Db: dbpool,
28+ }
29+}
30+
31+func (img *ImgsAPI) HasAccess(userID string) bool {
32+ return img.Db.HasFeatureForUser(userID, "imgs")
33+}
34+
35+func (img *ImgsAPI) Upload(s ssh.Session, file *utils.FileEntry) (string, error) {
36+ handler := uploadimgs.NewUploadImgHandler(img.Db, img.Cfg, storage.NewStorageFS(img.Cfg.StorageDir))
37+ err := handler.Validate(s)
38+ if err != nil {
39+ return "", err
40+ }
41+
42+ return handler.Write(s, file)
43+}
+9,
-0
1@@ -7,6 +7,15 @@ import (
2 "git.sr.ht/~erock/pico/wish/cms/config"
3 )
4
5+func ImgBaseURL(username string) string {
6+ cfg := NewConfigSite()
7+ if cfg.IsSubdomains() {
8+ return fmt.Sprintf("%s://%s.%s", cfg.Protocol, username, cfg.Domain)
9+ }
10+
11+ return "/"
12+}
13+
14 func NewConfigSite() *shared.ConfigSite {
15 domain := shared.GetEnv("IMGS_DOMAIN", "prose.sh")
16 email := shared.GetEnv("IMGS_EMAIL", "hello@prose.sh")
+1,
-1
1@@ -38,7 +38,7 @@
2 <h2 class="text-lg font-bold">Beta access</h2>
3 <p>
4 Want beta access? You must join our
5- <a href="irc://irc.libera.chat/#pico.sh">IRC channel</a> (#pico.sh on libera)
6+ <a href="https://web.libera.chat/gamja/?channels=%23pico.sh">IRC channel</a> (#pico.sh on libera)
7 and ask for access. We want all beta testers to be in IRC so you can provide us with
8 feedback.
9 </p>
R imgs/storage.go =>
imgs/storage/storage.go
+1,
-1
1@@ -1,4 +1,4 @@
2-package imgs
3+package storage
4
5 import (
6 "fmt"
+13,
-12
1@@ -13,6 +13,7 @@ import (
2
3 "git.sr.ht/~erock/pico/db"
4 "git.sr.ht/~erock/pico/db/postgres"
5+ "git.sr.ht/~erock/pico/imgs"
6 "git.sr.ht/~erock/pico/shared"
7 "github.com/gorilla/feeds"
8 "golang.org/x/exp/slices"
9@@ -191,7 +192,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
10 if post.Filename == "_styles.css" && len(post.Text) > 0 {
11 hasCSS = true
12 } else if post.Filename == "_readme.md" {
13- parsedText, err := shared.ParseText(post.Text)
14+ parsedText, err := shared.ParseText(post.Text, imgs.ImgBaseURL(post.Username))
15 if err != nil {
16 logger.Error(err)
17 }
18@@ -328,7 +329,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
19 var data PostPageData
20 post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
21 if err == nil {
22- parsedText, err := shared.ParseText(post.Text)
23+ parsedText, err := shared.ParseText(post.Text, imgs.ImgBaseURL(username))
24 if err != nil {
25 logger.Error(err)
26 }
27@@ -336,7 +337,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
28 // we need the blog name from the readme unfortunately
29 readme, err := dbpool.FindPostWithFilename("_readme.md", user.ID, cfg.Space)
30 if err == nil {
31- readmeParsed, err := shared.ParseText(readme.Text)
32+ readmeParsed, err := shared.ParseText(readme.Text, imgs.ImgBaseURL(username))
33 if err != nil {
34 logger.Error(err)
35 }
36@@ -540,9 +541,15 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
37 Title: GetBlogName(username),
38 }
39
40+ hostDomain := strings.Split(r.Host, ":")[0]
41+ appDomain := strings.Split(cfg.ConfigCms.Domain, ":")[0]
42+
43+ onSubdomain := cfg.IsSubdomains() && strings.Contains(hostDomain, appDomain)
44+ withUserName := (!onSubdomain && hostDomain == appDomain) || !cfg.IsCustomdomains()
45+
46 for _, post := range posts {
47 if post.Filename == "_readme.md" {
48- parsedText, err := shared.ParseText(post.Text)
49+ parsedText, err := shared.ParseText(post.Text, imgs.ImgBaseURL(post.Username))
50 if err != nil {
51 logger.Error(err)
52 }
53@@ -558,12 +565,6 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
54 }
55 }
56
57- hostDomain := strings.Split(r.Host, ":")[0]
58- appDomain := strings.Split(cfg.ConfigCms.Domain, ":")[0]
59-
60- onSubdomain := cfg.IsSubdomains() && strings.Contains(hostDomain, appDomain)
61- withUserName := (!onSubdomain && hostDomain == appDomain) || !cfg.IsCustomdomains()
62-
63 feed := &feeds.Feed{
64 Title: headerTxt.Title,
65 Link: &feeds.Link{Href: cfg.FullBlogURL(username, onSubdomain, withUserName)},
66@@ -577,7 +578,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
67 if slices.Contains(cfg.HiddenPosts, post.Filename) {
68 continue
69 }
70- parsed, err := shared.ParseText(post.Text)
71+ parsed, err := shared.ParseText(post.Text, imgs.ImgBaseURL(post.Username))
72 if err != nil {
73 logger.Error(err)
74 }
75@@ -660,7 +661,7 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
76
77 var feedItems []*feeds.Item
78 for _, post := range pager.Data {
79- parsed, err := shared.ParseText(post.Text)
80+ parsed, err := shared.ParseText(post.Text, imgs.ImgBaseURL(post.Username))
81 if err != nil {
82 logger.Error(err)
83 }
+1,
-1
1@@ -45,7 +45,7 @@ func (p *MarkdownHooks) FileValidate(data *filehandlers.PostMetaData) (bool, err
2 }
3
4 func (p *MarkdownHooks) FileMeta(data *filehandlers.PostMetaData) error {
5- parsedText, err := shared.ParseText(data.Text)
6+ parsedText, err := shared.ParseText(data.Text, "")
7 // we return nil here because we don't want the file upload to fail
8 if err != nil {
9 return nil
1@@ -143,6 +143,45 @@ func (c *ConfigSite) RawPostURL(username, slug string) string {
2 return fmt.Sprintf("/raw/%s/%s", username, fname)
3 }
4
5+func (c *ConfigSite) ImgURL(username string, slug string, onSubdomain bool, withUserName bool) string {
6+ fname := url.PathEscape(strings.TrimLeft(slug, "/"))
7+ if c.IsSubdomains() && onSubdomain {
8+ return fmt.Sprintf("%s://%s.%s/%s", c.Protocol, username, c.Domain, fname)
9+ }
10+
11+ if withUserName {
12+ return fmt.Sprintf("/%s/%s", username, fname)
13+ }
14+
15+ return fmt.Sprintf("/%s", fname)
16+}
17+
18+func (c *ConfigSite) TagURL(username, tag string, onSubdomain, withUserName bool) string {
19+ tg := url.PathEscape(tag)
20+ if c.IsSubdomains() && onSubdomain {
21+ return fmt.Sprintf("%s://%s.%s/t/%s", c.Protocol, username, c.Domain, tg)
22+ }
23+
24+ if withUserName {
25+ return fmt.Sprintf("/%s/t/%s", username, tg)
26+ }
27+
28+ return fmt.Sprintf("/t/%s", tg)
29+}
30+
31+func (c *ConfigSite) TagPostURL(username, tag, slug string, onSubdomain, withUserName bool) string {
32+ fname := url.PathEscape(strings.TrimLeft(slug, "/"))
33+ if c.IsSubdomains() && onSubdomain {
34+ return fmt.Sprintf("%s://%s.%s/%s/%s", c.Protocol, username, c.Domain, tag, fname)
35+ }
36+
37+ if withUserName {
38+ return fmt.Sprintf("/%s/%s/%s", username, tag, fname)
39+ }
40+
41+ return fmt.Sprintf("/%s/%s", tag, fname)
42+}
43+
44 func CreateLogger() *zap.SugaredLogger {
45 logger, err := zap.NewProduction()
46 if err != nil {
1@@ -12,9 +12,12 @@ 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 )
12
13 type Link struct {
14@@ -107,7 +110,65 @@ func toTags(obj interface{}) ([]string, error) {
15 return arr, nil
16 }
17
18-func ParseText(text string) (*ParsedText, error) {
19+type ImgRender struct {
20+ ghtml.Config
21+ ImgURL func(url []byte) []byte
22+}
23+
24+func NewImgsRenderer(url func([]byte) []byte) renderer.NodeRenderer {
25+ return &ImgRender{
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(absURL string) func([]byte) []byte {
64+ return func(url []byte) []byte {
65+ if url[0] == '/' {
66+ nextURL := fmt.Sprintf("%s%s", absURL, string(url))
67+ return []byte(nextURL)
68+ } else if bytes.HasPrefix(url, []byte{'.', '/'}) {
69+ fname := url[1:]
70+ nextURL := fmt.Sprintf("%s%s", absURL, string(fname))
71+ return []byte(nextURL)
72+ }
73+ return url
74+ }
75+}
76+
77+func ParseText(text string, absURL string) (*ParsedText, error) {
78 parsed := ParsedText{
79 MetaData: &MetaData{
80 Tags: []string{},
81@@ -131,6 +192,9 @@ func ParseText(text string) (*ParsedText, error) {
82 ),
83 goldmark.WithRendererOptions(
84 ghtml.WithUnsafe(),
85+ renderer.WithNodeRenderers(
86+ util.Prioritized(NewImgsRenderer(createImgURL(absURL)), 0),
87+ ),
88 ),
89 )
90 context := parser.NewContext()