Eric Bower
·
15 Nov 24
api.go
1package pastes
2
3import (
4 "fmt"
5 "html/template"
6 "net/http"
7 "net/url"
8 "os"
9 "time"
10
11 "github.com/picosh/pico/db"
12 "github.com/picosh/pico/db/postgres"
13 "github.com/picosh/pico/shared"
14 "github.com/picosh/pico/shared/storage"
15 "github.com/picosh/utils"
16)
17
18type PageData struct {
19 Site shared.SitePageData
20}
21
22type PostItemData struct {
23 URL template.URL
24 BlogURL template.URL
25 Username string
26 Title string
27 Description string
28 PublishAtISO string
29 PublishAt string
30 UpdatedAtISO string
31 UpdatedTimeAgo string
32 Padding string
33}
34
35type BlogPageData struct {
36 Site shared.SitePageData
37 PageTitle string
38 URL template.URL
39 RSSURL template.URL
40 Username string
41 Header *HeaderTxt
42 Posts []PostItemData
43}
44
45type PostPageData struct {
46 Site shared.SitePageData
47 PageTitle string
48 URL template.URL
49 RawURL template.URL
50 BlogURL template.URL
51 Title string
52 Description string
53 Username string
54 BlogName string
55 Contents template.HTML
56 PublishAtISO string
57 PublishAt string
58 ExpiresAt string
59 Unlisted bool
60}
61
62type Link struct {
63 URL string
64 Text string
65}
66
67type HeaderTxt struct {
68 Title string
69 Bio string
70 Nav []Link
71 HasLinks bool
72}
73
74func blogHandler(w http.ResponseWriter, r *http.Request) {
75 username := shared.GetUsernameFromRequest(r)
76 dbpool := shared.GetDB(r)
77 blogger := shared.GetLogger(r)
78 logger := blogger.With("user", username)
79 cfg := shared.GetCfg(r)
80
81 user, err := dbpool.FindUserForName(username)
82 if err != nil {
83 logger.Info("user not found")
84 http.Error(w, "user not found", http.StatusNotFound)
85 return
86 }
87 logger = shared.LoggerWithUser(blogger, user)
88
89 pager, err := dbpool.FindPostsForUser(&db.Pager{Num: 1000, Page: 0}, user.ID, cfg.Space)
90 if err != nil {
91 logger.Error("could not find posts for user", "err", err.Error())
92 http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError)
93 return
94 }
95
96 posts := pager.Data
97
98 ts, err := shared.RenderTemplate(cfg, []string{
99 cfg.StaticPath("html/blog.page.tmpl"),
100 })
101
102 if err != nil {
103 logger.Error("could not render template", "err", err)
104 http.Error(w, err.Error(), http.StatusInternalServerError)
105 return
106 }
107
108 headerTxt := &HeaderTxt{
109 Title: GetBlogName(username),
110 Bio: "",
111 }
112
113 curl := shared.CreateURLFromRequest(cfg, r)
114 postCollection := make([]PostItemData, 0, len(posts))
115 for _, post := range posts {
116 p := PostItemData{
117 URL: template.URL(cfg.FullPostURL(curl, post.Username, post.Slug)),
118 BlogURL: template.URL(cfg.FullBlogURL(curl, post.Username)),
119 Title: post.Filename,
120 PublishAt: post.PublishAt.Format(time.DateOnly),
121 PublishAtISO: post.PublishAt.Format(time.RFC3339),
122 UpdatedTimeAgo: utils.TimeAgo(post.UpdatedAt),
123 UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),
124 }
125 postCollection = append(postCollection, p)
126 }
127
128 data := BlogPageData{
129 Site: *cfg.GetSiteData(),
130 PageTitle: headerTxt.Title,
131 URL: template.URL(cfg.FullBlogURL(curl, username)),
132 RSSURL: template.URL(cfg.RssBlogURL(curl, username, "")),
133 Header: headerTxt,
134 Username: username,
135 Posts: postCollection,
136 }
137
138 err = ts.Execute(w, data)
139 if err != nil {
140 logger.Error("could not execute tempalte", "err", err)
141 http.Error(w, err.Error(), http.StatusInternalServerError)
142 }
143}
144
145func GetPostTitle(post *db.Post) string {
146 if post.Description == "" {
147 return post.Title
148 }
149
150 return fmt.Sprintf("%s: %s", post.Title, post.Description)
151}
152
153func GetBlogName(username string) string {
154 return fmt.Sprintf("%s's pastes", username)
155}
156
157func postHandler(w http.ResponseWriter, r *http.Request) {
158 username := shared.GetUsernameFromRequest(r)
159 subdomain := shared.GetSubdomain(r)
160 cfg := shared.GetCfg(r)
161
162 var slug string
163 if !cfg.IsSubdomains() || subdomain == "" {
164 slug, _ = url.PathUnescape(shared.GetField(r, 1))
165 } else {
166 slug, _ = url.PathUnescape(shared.GetField(r, 0))
167 }
168
169 dbpool := shared.GetDB(r)
170 blogger := shared.GetLogger(r)
171 logger := blogger.With("slug", slug, "user", username)
172
173 user, err := dbpool.FindUserForName(username)
174 if err != nil {
175 logger.Info("paste not found")
176 http.Error(w, "paste not found", http.StatusNotFound)
177 return
178 }
179 logger = shared.LoggerWithUser(logger, user)
180
181 blogName := GetBlogName(username)
182
183 var data PostPageData
184 post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
185 if err == nil {
186 logger = logger.With("filename", post.Filename)
187 logger.Info("paste found")
188 parsedText, err := ParseText(post.Filename, post.Text)
189 if err != nil {
190 logger.Error("could not parse text", "err", err)
191 }
192 expiresAt := "never"
193 if post.ExpiresAt != nil {
194 expiresAt = post.ExpiresAt.Format(time.DateOnly)
195 }
196
197 unlisted := false
198 if post.Hidden {
199 unlisted = true
200 }
201
202 data = PostPageData{
203 Site: *cfg.GetSiteData(),
204 PageTitle: post.Filename,
205 URL: template.URL(cfg.PostURL(post.Username, post.Slug)),
206 RawURL: template.URL(cfg.RawPostURL(post.Username, post.Slug)),
207 BlogURL: template.URL(cfg.BlogURL(username)),
208 Description: post.Description,
209 Title: post.Filename,
210 PublishAt: post.PublishAt.Format(time.DateOnly),
211 PublishAtISO: post.PublishAt.Format(time.RFC3339),
212 Username: username,
213 BlogName: blogName,
214 Contents: template.HTML(parsedText),
215 ExpiresAt: expiresAt,
216 Unlisted: unlisted,
217 }
218 } else {
219 logger.Info("paste not found")
220 data = PostPageData{
221 Site: *cfg.GetSiteData(),
222 PageTitle: "Paste not found",
223 Description: "Paste not found",
224 Title: "Paste not found",
225 BlogURL: template.URL(cfg.BlogURL(username)),
226 PublishAt: time.Now().Format(time.DateOnly),
227 PublishAtISO: time.Now().Format(time.RFC3339),
228 Username: username,
229 BlogName: blogName,
230 Contents: "oops! we can't seem to find this post.",
231 ExpiresAt: "",
232 }
233 }
234
235 ts, err := shared.RenderTemplate(cfg, []string{
236 cfg.StaticPath("html/post.page.tmpl"),
237 })
238
239 if err != nil {
240 http.Error(w, err.Error(), http.StatusInternalServerError)
241 }
242
243 logger.Info("serving paste")
244 err = ts.Execute(w, data)
245 if err != nil {
246 logger.Error("could not execute template", "err", err)
247 http.Error(w, err.Error(), http.StatusInternalServerError)
248 }
249}
250
251func postHandlerRaw(w http.ResponseWriter, r *http.Request) {
252 username := shared.GetUsernameFromRequest(r)
253 subdomain := shared.GetSubdomain(r)
254 cfg := shared.GetCfg(r)
255
256 var slug string
257 if !cfg.IsSubdomains() || subdomain == "" {
258 slug, _ = url.PathUnescape(shared.GetField(r, 1))
259 } else {
260 slug, _ = url.PathUnescape(shared.GetField(r, 0))
261 }
262
263 dbpool := shared.GetDB(r)
264 blogger := shared.GetLogger(r)
265 logger := blogger.With("user", username, "slug", slug)
266
267 user, err := dbpool.FindUserForName(username)
268 if err != nil {
269 logger.Info("user not found")
270 http.Error(w, "user not found", http.StatusNotFound)
271 return
272 }
273 logger = shared.LoggerWithUser(blogger, user)
274
275 post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
276 if err != nil {
277 logger.Info("paste not found")
278 http.Error(w, "paste not found", http.StatusNotFound)
279 return
280 }
281 logger = logger.With("filename", post.Filename)
282 logger.Info("raw paste found")
283
284 w.Header().Set("Content-Type", "text/plain")
285 _, err = w.Write([]byte(post.Text))
286 if err != nil {
287 logger.Error("write error", "err", err)
288 }
289}
290
291func serveFile(file string, contentType string) http.HandlerFunc {
292 return func(w http.ResponseWriter, r *http.Request) {
293 logger := shared.GetLogger(r)
294 cfg := shared.GetCfg(r)
295
296 contents, err := os.ReadFile(cfg.StaticPath(fmt.Sprintf("public/%s", file)))
297 if err != nil {
298 logger.Error("could not read file", "err", err)
299 http.Error(w, "file not found", 404)
300 }
301 w.Header().Add("Content-Type", contentType)
302
303 _, err = w.Write(contents)
304 if err != nil {
305 logger.Error("could not write contents", "err", err)
306 http.Error(w, "server error", 500)
307 }
308 }
309}
310
311func createStaticRoutes() []shared.Route {
312 return []shared.Route{
313 shared.NewRoute("GET", "/main.css", serveFile("main.css", "text/css")),
314 shared.NewRoute("GET", "/smol.css", serveFile("smol.css", "text/css")),
315 shared.NewRoute("GET", "/syntax.css", serveFile("syntax.css", "text/css")),
316 shared.NewRoute("GET", "/card.png", serveFile("card.png", "image/png")),
317 shared.NewRoute("GET", "/favicon-16x16.png", serveFile("favicon-16x16.png", "image/png")),
318 shared.NewRoute("GET", "/favicon-32x32.png", serveFile("favicon-32x32.png", "image/png")),
319 shared.NewRoute("GET", "/apple-touch-icon.png", serveFile("apple-touch-icon.png", "image/png")),
320 shared.NewRoute("GET", "/favicon.ico", serveFile("favicon.ico", "image/x-icon")),
321 shared.NewRoute("GET", "/robots.txt", serveFile("robots.txt", "text/plain")),
322 }
323}
324
325func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
326 routes := []shared.Route{
327 shared.NewRoute("GET", "/", shared.CreatePageHandler("html/marketing.page.tmpl")),
328 shared.NewRoute("GET", "/check", shared.CheckHandler),
329 }
330
331 routes = append(
332 routes,
333 staticRoutes...,
334 )
335
336 routes = append(
337 routes,
338 shared.NewRoute("GET", "/([^/]+)", blogHandler),
339 shared.NewRoute("GET", "/([^/]+)/([^/]+)", postHandler),
340 shared.NewRoute("GET", "/([^/]+)/([^/]+)/raw", postHandlerRaw),
341 shared.NewRoute("GET", "/raw/([^/]+)/([^/]+)", postHandlerRaw),
342 )
343
344 return routes
345}
346
347func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
348 routes := []shared.Route{
349 shared.NewRoute("GET", "/", blogHandler),
350 }
351
352 routes = append(
353 routes,
354 staticRoutes...,
355 )
356
357 routes = append(
358 routes,
359 shared.NewRoute("GET", "/([^/]+)", postHandler),
360 shared.NewRoute("GET", "/([^/]+)/raw", postHandlerRaw),
361 shared.NewRoute("GET", "/raw/([^/]+)", postHandlerRaw),
362 )
363
364 return routes
365}
366
367func StartApiServer() {
368 cfg := NewConfigSite()
369 db := postgres.NewDB(cfg.DbURL, cfg.Logger)
370 defer db.Close()
371 logger := cfg.Logger
372
373 var st storage.StorageServe
374 var err error
375 if cfg.MinioURL == "" {
376 st, err = storage.NewStorageFS(cfg.StorageDir)
377 } else {
378 st, err = storage.NewStorageMinio(cfg.MinioURL, cfg.MinioUser, cfg.MinioPass)
379 }
380
381 if err != nil {
382 logger.Error("could not create storage adapter", "err", err.Error())
383 return
384 }
385
386 go CronDeleteExpiredPosts(cfg, db)
387
388 staticRoutes := createStaticRoutes()
389
390 if cfg.Debug {
391 staticRoutes = shared.CreatePProfRoutes(staticRoutes)
392 }
393
394 mainRoutes := createMainRoutes(staticRoutes)
395 subdomainRoutes := createSubdomainRoutes(staticRoutes)
396
397 apiConfig := &shared.ApiConfig{
398 Cfg: cfg,
399 Dbpool: db,
400 Storage: st,
401 }
402 handler := shared.CreateServe(mainRoutes, subdomainRoutes, apiConfig)
403 router := http.HandlerFunc(handler)
404
405 portStr := fmt.Sprintf(":%s", cfg.Port)
406 logger.Info(
407 "Starting server on port",
408 "port", cfg.Port,
409 "domain", cfg.Domain,
410 )
411
412 logger.Error(http.ListenAndServe(portStr, router).Error())
413}