Eric Bower
·
15 Nov 24
api.go
1package imgs
2
3import (
4 "bytes"
5 "fmt"
6 "html/template"
7 "net/http"
8 "net/url"
9 "path/filepath"
10 "time"
11
12 _ "net/http/pprof"
13
14 "github.com/gorilla/feeds"
15 "github.com/picosh/pico/db"
16 "github.com/picosh/pico/db/postgres"
17 "github.com/picosh/pico/pgs"
18 "github.com/picosh/pico/shared"
19 "github.com/picosh/pico/shared/storage"
20 "github.com/picosh/utils"
21)
22
23type PostPageData struct {
24 ImgURL template.URL
25}
26
27type BlogPageData struct {
28 Site *shared.SitePageData
29 PageTitle string
30 URL template.URL
31 Username string
32 Posts []template.URL
33}
34
35var Space = "imgs"
36
37func ImgsListHandler(w http.ResponseWriter, r *http.Request) {
38 username := shared.GetUsernameFromRequest(r)
39 dbpool := shared.GetDB(r)
40 logger := shared.GetLogger(r)
41 cfg := shared.GetCfg(r)
42
43 user, err := dbpool.FindUserForName(username)
44 if err != nil {
45 logger.Info("blog not found", "username", username)
46 http.Error(w, "blog not found", http.StatusNotFound)
47 return
48 }
49
50 var posts []*db.Post
51 pager := &db.Pager{Num: 1000, Page: 0}
52 p, err := dbpool.FindPostsForUser(pager, user.ID, Space)
53 posts = p.Data
54
55 if err != nil {
56 logger.Error(err.Error())
57 http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError)
58 return
59 }
60
61 ts, err := shared.RenderTemplate(cfg, []string{
62 cfg.StaticPath("html/imgs.page.tmpl"),
63 })
64
65 if err != nil {
66 logger.Error(err.Error())
67 http.Error(w, err.Error(), http.StatusInternalServerError)
68 return
69 }
70
71 curl := shared.CreateURLFromRequest(cfg, r)
72 postCollection := make([]template.URL, 0, len(posts))
73 for _, post := range posts {
74 url := cfg.ImgURL(curl, post.Username, post.Slug)
75 postCollection = append(postCollection, template.URL(url))
76 }
77
78 data := BlogPageData{
79 Site: cfg.GetSiteData(),
80 PageTitle: fmt.Sprintf("%s imgs", username),
81 URL: template.URL(cfg.FullBlogURL(curl, username)),
82 Username: username,
83 Posts: postCollection,
84 }
85
86 err = ts.Execute(w, data)
87 if err != nil {
88 logger.Error(err.Error())
89 http.Error(w, err.Error(), http.StatusInternalServerError)
90 }
91}
92
93func ImgsRssHandler(w http.ResponseWriter, r *http.Request) {
94 dbpool := shared.GetDB(r)
95 logger := shared.GetLogger(r)
96 cfg := shared.GetCfg(r)
97
98 pager, err := dbpool.FindAllPosts(&db.Pager{Num: 25, Page: 0}, Space)
99 if err != nil {
100 logger.Error(err.Error())
101 http.Error(w, err.Error(), http.StatusInternalServerError)
102 return
103 }
104
105 ts, err := template.ParseFiles(cfg.StaticPath("html/rss.page.tmpl"))
106 if err != nil {
107 logger.Error(err.Error())
108 http.Error(w, err.Error(), http.StatusInternalServerError)
109 return
110 }
111
112 feed := &feeds.Feed{
113 Title: fmt.Sprintf("%s imgs feed", cfg.Domain),
114 Link: &feeds.Link{Href: cfg.HomeURL()},
115 Description: fmt.Sprintf("%s latest image", cfg.Domain),
116 Author: &feeds.Author{Name: cfg.Domain},
117 Created: time.Now(),
118 }
119
120 curl := shared.CreateURLFromRequest(cfg, r)
121
122 var feedItems []*feeds.Item
123 for _, post := range pager.Data {
124 var tpl bytes.Buffer
125 data := &PostPageData{
126 ImgURL: template.URL(cfg.ImgURL(curl, post.Username, post.Filename)),
127 }
128 if err := ts.Execute(&tpl, data); err != nil {
129 continue
130 }
131
132 realUrl := cfg.FullPostURL(curl, post.Username, post.Filename)
133 if !curl.Subdomain && !curl.UsernameInRoute {
134 realUrl = fmt.Sprintf("%s://%s%s", cfg.Protocol, r.Host, realUrl)
135 }
136
137 item := &feeds.Item{
138 Id: realUrl,
139 Title: post.Title,
140 Link: &feeds.Link{Href: realUrl},
141 Content: tpl.String(),
142 Created: *post.PublishAt,
143 Updated: *post.UpdatedAt,
144 Description: post.Description,
145 Author: &feeds.Author{Name: post.Username},
146 }
147
148 if post.Description != "" {
149 item.Description = post.Description
150 }
151
152 feedItems = append(feedItems, item)
153 }
154 feed.Items = feedItems
155
156 rss, err := feed.ToAtom()
157 if err != nil {
158 logger.Error(err.Error())
159 http.Error(w, "Could not generate atom rss feed", http.StatusInternalServerError)
160 }
161
162 w.Header().Add("Content-Type", "application/atom+xml")
163 _, err = w.Write([]byte(rss))
164 if err != nil {
165 logger.Error(err.Error())
166 }
167}
168
169func anyPerm(proj *db.Project) bool {
170 return true
171}
172
173func ImgRequest(w http.ResponseWriter, r *http.Request) {
174 subdomain := shared.GetSubdomain(r)
175 cfg := shared.GetCfg(r)
176 st := shared.GetStorage(r)
177 dbpool := shared.GetDB(r)
178 logger := shared.GetLogger(r)
179 username := shared.GetUsernameFromRequest(r)
180
181 user, err := dbpool.FindUserForName(username)
182 if err != nil {
183 logger.Info("user not found", "user", username)
184 http.Error(w, "user not found", http.StatusNotFound)
185 return
186 }
187
188 var imgOpts string
189 var slug string
190 if !cfg.IsSubdomains() || subdomain == "" {
191 slug, _ = url.PathUnescape(shared.GetField(r, 1))
192 imgOpts, _ = url.PathUnescape(shared.GetField(r, 2))
193 } else {
194 slug, _ = url.PathUnescape(shared.GetField(r, 0))
195 imgOpts, _ = url.PathUnescape(shared.GetField(r, 1))
196 }
197
198 opts, err := storage.UriToImgProcessOpts(imgOpts)
199 if err != nil {
200 errMsg := fmt.Sprintf("error processing img options: %s", err.Error())
201 logger.Info(errMsg)
202 http.Error(w, errMsg, http.StatusUnprocessableEntity)
203 return
204 }
205
206 // set default quality for web optimization
207 if opts.Quality == 0 {
208 opts.Quality = 80
209 }
210
211 ext := filepath.Ext(slug)
212 // set default format to be webp
213 if opts.Ext == "" && ext == "" {
214 opts.Ext = "webp"
215 }
216
217 // Files can contain periods. `filepath.Ext` is greedy and will clip the last period in the slug
218 // and call that a file extension so we want to be explicit about what
219 // file extensions we clip here
220 for _, fext := range cfg.AllowedExt {
221 if ext == fext {
222 // users might add the file extension when requesting an image
223 // but we want to remove that
224 slug = utils.SanitizeFileExt(slug)
225 break
226 }
227 }
228
229 post, err := FindImgPost(r, user, slug)
230 if err != nil {
231 errMsg := fmt.Sprintf("image not found %s/%s", user.Name, slug)
232 logger.Info(errMsg)
233 http.Error(w, errMsg, http.StatusNotFound)
234 return
235 }
236
237 fname := post.Filename
238 router := pgs.NewWebRouter(
239 cfg,
240 logger,
241 dbpool,
242 st,
243 )
244 router.ServeAsset(fname, opts, true, anyPerm, w, r)
245}
246
247func FindImgPost(r *http.Request, user *db.User, slug string) (*db.Post, error) {
248 dbpool := shared.GetDB(r)
249 return dbpool.FindPostWithSlug(slug, user.ID, Space)
250}
251
252func redirectHandler(w http.ResponseWriter, r *http.Request) {
253 username := shared.GetUsernameFromRequest(r)
254 url := fmt.Sprintf("https://%s.prose.sh/i", username)
255 http.Redirect(w, r, url, http.StatusMovedPermanently)
256}
257
258func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
259 routes := []shared.Route{
260 shared.NewRoute("GET", "/check", shared.CheckHandler),
261 shared.NewRoute("GET", "/", func(w http.ResponseWriter, r *http.Request) {
262 http.Redirect(w, r, "https://prose.sh", http.StatusMovedPermanently)
263 }),
264 }
265
266 routes = append(
267 routes,
268 staticRoutes...,
269 )
270
271 routes = append(
272 routes,
273 shared.NewRoute("GET", "/rss", ImgsRssHandler),
274 shared.NewRoute("GET", "/rss.xml", ImgsRssHandler),
275 shared.NewRoute("GET", "/atom.xml", ImgsRssHandler),
276 shared.NewRoute("GET", "/feed.xml", ImgsRssHandler),
277
278 shared.NewRoute("GET", "/([^/]+)", redirectHandler),
279 shared.NewRoute("GET", "/([^/]+)/o/([^/]+)", ImgRequest),
280 shared.NewRoute("GET", "/([^/]+)/([^/]+)", ImgRequest),
281 shared.NewRoute("GET", "/([^/]+)/([^/]+)/(.+)", ImgRequest),
282 )
283
284 return routes
285}
286
287func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
288 routes := []shared.Route{}
289
290 routes = append(
291 routes,
292 staticRoutes...,
293 )
294
295 routes = append(
296 routes,
297 shared.NewRoute("GET", "/", redirectHandler),
298 shared.NewRoute("GET", "/o/([^/]+)", ImgRequest),
299 shared.NewRoute("GET", "/([^/]+)", ImgRequest),
300 shared.NewRoute("GET", "/([^/]+)/(.+)", ImgRequest),
301 )
302
303 return routes
304}
305
306func StartApiServer() {
307 cfg := NewConfigSite()
308 logger := cfg.Logger
309
310 db := postgres.NewDB(cfg.DbURL, cfg.Logger)
311 defer db.Close()
312
313 var st storage.StorageServe
314 var err error
315 if cfg.MinioURL == "" {
316 st, err = storage.NewStorageFS(cfg.StorageDir)
317 } else {
318 st, err = storage.NewStorageMinio(cfg.MinioURL, cfg.MinioUser, cfg.MinioPass)
319 }
320
321 if err != nil {
322 logger.Error(err.Error())
323 }
324
325 staticRoutes := []shared.Route{}
326 if cfg.Debug {
327 staticRoutes = shared.CreatePProfRoutes(staticRoutes)
328 }
329
330 mainRoutes := createMainRoutes(staticRoutes)
331 subdomainRoutes := createSubdomainRoutes(staticRoutes)
332
333 apiConfig := &shared.ApiConfig{
334 Cfg: cfg,
335 Dbpool: db,
336 Storage: st,
337 }
338 handler := shared.CreateServe(mainRoutes, subdomainRoutes, apiConfig)
339 router := http.HandlerFunc(handler)
340
341 portStr := fmt.Sprintf(":%s", cfg.Port)
342 logger.Info(
343 "Starting server on port",
344 "port", cfg.Port,
345 "domain", cfg.Domain,
346 )
347
348 logger.Error(http.ListenAndServe(portStr, router).Error())
349}