repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

pico / pastes
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}