Commit c1156d3

Antonio Mika  ·  2026-10-10 10:18:08 -0400 EDT
parent 4c5cdcb
feat(pastes): readd pastes service
30 files changed,  +2527, -10
+4, -2
......@@ -12,9 +12,11 @@ WRITE?=0
1212 smol:
1313 curl https://pico.sh/smol.css -o ./pkg/apps/prose/public/smol-v2.css
1414 cat ./pkg/apps/prose/artifacts/main.css >> ./pkg/apps/prose/public/smol-v2.css
15+ curl https://pico.sh/smol.css -o ./pkg/apps/pastes/public/smol.css
1516 .PHONY: smol
1617
1718 css:
19+ cp ./syntax.css ./pkg/apps/pastes/public/syntax.css
1820 cp ./syntax.css ./pkg/apps/prose/public/syntax.css
1921 .PHONY: css
2022
......@@ -80,7 +82,7 @@ bp-%: bp-setup
8082 $(DOCKER_BUILDX_BUILD) "ghcr.io/picosh/pico/$*-web:$(DOCKER_TAG)" --build-arg "APP=$*" --target release-web .
8183 .PHONY: bp-%
8284
83-bp-all: bp-prose bp-feeds bp-pgs bp-auth bp-bouncer bp-pipe bp-pgs-cdn
85+bp-all: bp-prose bp-pastes bp-feeds bp-pgs bp-auth bp-bouncer bp-pipe bp-pgs-cdn
8486 .PHONY: bp-all
8587
8688 LDFLAGS?=-s -w
......@@ -102,7 +104,7 @@ build-%:
102104 go build -ldflags="$(LDFLAGS)" -o "build/$*-ssh" "./cmd/$*/ssh"
103105 .PHONY: build-%
104106
105-build: build-prose build-feeds build-pgs build-pgs-cdn build-auth build-pico build-pipe
107+build: build-prose build-pastes build-feeds build-pgs build-pgs-cdn build-auth build-pico build-pipe
106108 .PHONY: build
107109
108110 scripts:
+14, -1
......@@ -12,7 +12,8 @@ without needing to install anything. We accomplish this with the `ssh` tools you
1212 already have installed on your system.
1313
1414 Want to publish a blog post? Use rsync, scp, or sftp. Want to publish a website?
15-Use rsync, scp, or sftp. Hopefully you see the trend.
15+Use rsync, scp, or sftp. Want to share a code snippet with a colleague? Use
16+rsync, scp, or sftp. Hopefully you see the trend.
1617
1718 - [pages](https://pico.sh/pgs): A static site hosting platform using `ssh` for
1819 site deployments. `ssh`.
......@@ -21,6 +22,8 @@ Use rsync, scp, or sftp. Hopefully you see the trend.
2122 - [prose](https://pico.sh/prose): A blog platform using `ssh` for content
2223 management.
2324 - [rss-to-email](https://pico.sh/feeds): An RSS email notification service using `ssh`.
25+- [pastes](https://pico.sh/pastes): Upload code snippets using rsync, scp, and
26+ sftp.
2427
2528 ## Deploy a site with a single command
2629
......@@ -78,6 +81,16 @@ scp hello-world.md prose.sh:/
7881 Congrats! You just published a blog article, accessible here:
7982 https://{user}.prose.sh/hello-world
8083
84+## Easily share code snippets
85+
86+Pipe some stdout to us:
87+
88+```bash
89+git diff | ssh pastes.sh changes.patch
90+```
91+
92+And instantly share your code snippets: https://{user}.pastes.sh/changes.patch
93+
8194 ## Receive email notifications for your favorite rss feeds
8295
8396 Create a blogs.txt file:
+7, -0
......@@ -0,0 +1,7 @@
1+package main
2+
3+import "github.com/picosh/pico/pkg/apps/pastes"
4+
5+func main() {
6+ pastes.StartSshServer()
7+}
+7, -0
......@@ -0,0 +1,7 @@
1+package main
2+
3+import "github.com/picosh/pico/pkg/apps/pastes"
4+
5+func main() {
6+ pastes.StartApiServer()
7+}
+1, -1
......@@ -33,7 +33,7 @@ zmx run cdn docker buildx build --push --platform "$DOCKER_PLATFORM" -t
3333 zmx run standalone docker buildx build --push --platform "$DOCKER_PLATFORM" -t "ghcr.io/picosh/pgs:$DOCKER_TAG" --target release -f Dockerfile.standalone .
3434 zmx wait "*"
3535
36-apps=("prose" "pgs" "feeds" "pipe")
36+apps=("prose" "pastes" "pgs" "feeds" "pipe")
3737 for APP in "${apps[@]}"; do
3838 zmx run "$APP-ssh" -d docker buildx build --push --platform "$DOCKER_PLATFORM" -t "ghcr.io/picosh/pico/$APP-ssh:$DOCKER_TAG" --build-arg "APP=$APP" --target release-ssh .
3939 zmx run "$APP-web" -d docker buildx build --push --platform "$DOCKER_PLATFORM" -t "ghcr.io/picosh/pico/$APP-web:$DOCKER_TAG" --build-arg "APP=$APP" --target release-web .
+412, -0
......@@ -0,0 +1,412 @@
1+package pastes
2+
3+import (
4+ "fmt"
5+ "html/template"
6+ "net/http"
7+ "net/url"
8+ "os"
9+ "time"
10+
11+ "github.com/picosh/pico/pkg/db"
12+ "github.com/picosh/pico/pkg/db/postgres"
13+ "github.com/picosh/pico/pkg/shared"
14+ "github.com/picosh/pico/pkg/shared/router"
15+ "github.com/prometheus/client_golang/prometheus/promhttp"
16+)
17+
18+type PageData struct {
19+ Site shared.SitePageData
20+}
21+
22+type 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+
35+type 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+
45+type 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+ Text string
57+ PublishAtISO string
58+ PublishAt string
59+ ExpiresAt string
60+ Unlisted bool
61+}
62+
63+type Link struct {
64+ URL string
65+ Text string
66+}
67+
68+type HeaderTxt struct {
69+ Title string
70+ Bio string
71+ Nav []Link
72+ HasLinks bool
73+}
74+
75+func blogHandler(w http.ResponseWriter, r *http.Request) {
76+ username := router.GetUsernameFromRequest(r)
77+ dbpool := router.GetDB(r)
78+ blogger := router.GetLogger(r)
79+ logger := blogger.With("user", username)
80+ cfg := router.GetCfg(r)
81+
82+ user, err := dbpool.FindUserByName(username)
83+ if err != nil {
84+ logger.Info("user not found")
85+ http.Error(w, "user not found", http.StatusNotFound)
86+ return
87+ }
88+ logger = shared.LoggerWithUser(blogger, user)
89+
90+ pager, err := dbpool.FindPostsByUser(&db.Pager{Num: 1000, Page: 0}, user.ID, cfg.Space)
91+ if err != nil {
92+ logger.Error("could not find posts for user", "err", err.Error())
93+ http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError)
94+ return
95+ }
96+
97+ posts := pager.Data
98+
99+ ts, err := router.RenderTemplate(cfg, []string{
100+ cfg.StaticPath("html/blog.page.tmpl"),
101+ })
102+
103+ if err != nil {
104+ logger.Error("could not render template", "err", err)
105+ http.Error(w, err.Error(), http.StatusInternalServerError)
106+ return
107+ }
108+
109+ headerTxt := &HeaderTxt{
110+ Title: GetBlogName(username),
111+ Bio: "",
112+ }
113+
114+ curl := shared.CreateURLFromRequest(cfg, r)
115+ postCollection := make([]PostItemData, 0, len(posts))
116+ for _, post := range posts {
117+ p := PostItemData{
118+ URL: template.URL(cfg.FullPostURL(curl, post.Username, post.Slug)),
119+ BlogURL: template.URL(cfg.FullBlogURL(curl, post.Username)),
120+ Title: post.Filename,
121+ PublishAt: post.PublishAt.Format(time.DateOnly),
122+ PublishAtISO: post.PublishAt.Format(time.RFC3339),
123+ UpdatedTimeAgo: shared.TimeAgo(post.UpdatedAt),
124+ UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),
125+ }
126+ postCollection = append(postCollection, p)
127+ }
128+
129+ data := BlogPageData{
130+ Site: *cfg.GetSiteData(),
131+ PageTitle: headerTxt.Title,
132+ URL: template.URL(cfg.FullBlogURL(curl, username)),
133+ RSSURL: template.URL(cfg.RssBlogURL(curl, username, "")),
134+ Header: headerTxt,
135+ Username: username,
136+ Posts: postCollection,
137+ }
138+
139+ err = ts.Execute(w, data)
140+ if err != nil {
141+ logger.Error("could not execute tempalte", "err", err)
142+ http.Error(w, err.Error(), http.StatusInternalServerError)
143+ }
144+}
145+
146+func GetPostTitle(post *db.Post) string {
147+ if post.Description == "" {
148+ return post.Title
149+ }
150+
151+ return fmt.Sprintf("%s: %s", post.Title, post.Description)
152+}
153+
154+func GetBlogName(username string) string {
155+ return fmt.Sprintf("%s's pastes", username)
156+}
157+
158+func postHandler(w http.ResponseWriter, r *http.Request) {
159+ username := router.GetUsernameFromRequest(r)
160+ subdomain := router.GetSubdomain(r)
161+ cfg := router.GetCfg(r)
162+
163+ var slug string
164+ if !cfg.IsSubdomains() || subdomain == "" {
165+ slug, _ = url.PathUnescape(router.GetField(r, 1))
166+ } else {
167+ slug, _ = url.PathUnescape(router.GetField(r, 0))
168+ }
169+
170+ dbpool := router.GetDB(r)
171+ blogger := router.GetLogger(r)
172+ logger := blogger.With("slug", slug, "user", username)
173+
174+ user, err := dbpool.FindUserByName(username)
175+ if err != nil {
176+ logger.Info("paste not found")
177+ http.Error(w, "paste not found", http.StatusNotFound)
178+ return
179+ }
180+ logger = shared.LoggerWithUser(logger, user)
181+
182+ blogName := GetBlogName(username)
183+
184+ var data PostPageData
185+ post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
186+ if err == nil {
187+ logger = logger.With("filename", post.Filename)
188+ logger.Info("paste found")
189+ expiresAt := "never"
190+ unlisted := false
191+ parsedText := ""
192+ // we dont want to syntax highlight huge files
193+ if post.FileSize > 1*shared.MB {
194+ logger.Warn("paste too large to parse and apply syntax highlighting")
195+ parsedText = post.Text
196+ } else {
197+ parsedText, err = ParseText(post.Filename, post.Text)
198+ if err != nil {
199+ logger.Error("could not parse text", "err", err)
200+ }
201+ if post.ExpiresAt != nil {
202+ expiresAt = post.ExpiresAt.Format(time.DateOnly)
203+ }
204+
205+ if post.Hidden {
206+ unlisted = true
207+ }
208+ }
209+
210+ data = PostPageData{
211+ Site: *cfg.GetSiteData(),
212+ PageTitle: post.Filename,
213+ URL: template.URL(cfg.PostURL(post.Username, post.Slug)),
214+ RawURL: template.URL(cfg.RawPostURL(post.Username, post.Slug)),
215+ BlogURL: template.URL(cfg.BlogURL(username)),
216+ Description: post.Description,
217+ Title: post.Filename,
218+ PublishAt: post.PublishAt.Format(time.DateOnly),
219+ PublishAtISO: post.PublishAt.Format(time.RFC3339),
220+ Username: username,
221+ BlogName: blogName,
222+ Contents: template.HTML(parsedText),
223+ Text: post.Text,
224+ ExpiresAt: expiresAt,
225+ Unlisted: unlisted,
226+ }
227+ } else {
228+ logger.Info("paste not found")
229+ data = PostPageData{
230+ Site: *cfg.GetSiteData(),
231+ PageTitle: "Paste not found",
232+ Description: "Paste not found",
233+ Title: "Paste not found",
234+ BlogURL: template.URL(cfg.BlogURL(username)),
235+ PublishAt: time.Now().Format(time.DateOnly),
236+ PublishAtISO: time.Now().Format(time.RFC3339),
237+ Username: username,
238+ BlogName: blogName,
239+ Contents: "oops! we can't seem to find this post.",
240+ Text: "oops! we can't seem to find this post.",
241+ ExpiresAt: "",
242+ }
243+ }
244+
245+ ts, err := router.RenderTemplate(cfg, []string{
246+ cfg.StaticPath("html/post.page.tmpl"),
247+ })
248+
249+ if err != nil {
250+ http.Error(w, err.Error(), http.StatusInternalServerError)
251+ }
252+
253+ logger.Info("serving paste")
254+ err = ts.Execute(w, data)
255+ if err != nil {
256+ logger.Error("could not execute template", "err", err)
257+ http.Error(w, err.Error(), http.StatusInternalServerError)
258+ }
259+}
260+
261+func postHandlerRaw(w http.ResponseWriter, r *http.Request) {
262+ username := router.GetUsernameFromRequest(r)
263+ subdomain := router.GetSubdomain(r)
264+ cfg := router.GetCfg(r)
265+
266+ var slug string
267+ if !cfg.IsSubdomains() || subdomain == "" {
268+ slug, _ = url.PathUnescape(router.GetField(r, 1))
269+ } else {
270+ slug, _ = url.PathUnescape(router.GetField(r, 0))
271+ }
272+
273+ dbpool := router.GetDB(r)
274+ blogger := router.GetLogger(r)
275+ logger := blogger.With("user", username, "slug", slug)
276+
277+ user, err := dbpool.FindUserByName(username)
278+ if err != nil {
279+ logger.Info("user not found")
280+ http.Error(w, "user not found", http.StatusNotFound)
281+ return
282+ }
283+ logger = shared.LoggerWithUser(blogger, user)
284+
285+ post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
286+ if err != nil {
287+ logger.Info("paste not found")
288+ http.Error(w, "paste not found", http.StatusNotFound)
289+ return
290+ }
291+ logger = logger.With("filename", post.Filename)
292+ logger.Info("raw paste found")
293+
294+ w.Header().Set("Content-Type", "text/plain")
295+ _, err = w.Write([]byte(post.Text))
296+ if err != nil {
297+ logger.Error("write error", "err", err)
298+ }
299+}
300+
301+func serveFile(file string, contentType string) http.HandlerFunc {
302+ return func(w http.ResponseWriter, r *http.Request) {
303+ logger := router.GetLogger(r)
304+ cfg := router.GetCfg(r)
305+
306+ contents, err := os.ReadFile(cfg.StaticPath(fmt.Sprintf("public/%s", file)))
307+ if err != nil {
308+ logger.Error("could not read file", "err", err)
309+ http.Error(w, "file not found", 404)
310+ }
311+ w.Header().Add("Content-Type", contentType)
312+
313+ _, err = w.Write(contents)
314+ if err != nil {
315+ logger.Error("could not write contents", "err", err)
316+ http.Error(w, "server error", 500)
317+ }
318+ }
319+}
320+
321+func createStaticRoutes() []router.Route {
322+ return []router.Route{
323+ router.NewRoute("GET", "/main.css", serveFile("main.css", "text/css")),
324+ router.NewRoute("GET", "/smol.css", serveFile("smol.css", "text/css")),
325+ router.NewRoute("GET", "/syntax.css", serveFile("syntax.css", "text/css")),
326+ router.NewRoute("GET", "/card.png", serveFile("card.png", "image/png")),
327+ router.NewRoute("GET", "/favicon-16x16.png", serveFile("favicon-16x16.png", "image/png")),
328+ router.NewRoute("GET", "/favicon-32x32.png", serveFile("favicon-32x32.png", "image/png")),
329+ router.NewRoute("GET", "/apple-touch-icon.png", serveFile("apple-touch-icon.png", "image/png")),
330+ router.NewRoute("GET", "/favicon.ico", serveFile("favicon.ico", "image/x-icon")),
331+ router.NewRoute("GET", "/robots.txt", serveFile("robots.txt", "text/plain")),
332+ }
333+}
334+
335+func createMainRoutes(staticRoutes []router.Route) []router.Route {
336+ routes := []router.Route{
337+ router.NewRoute("GET", "/", router.CreatePageHandler("html/marketing.page.tmpl")),
338+ router.NewRoute("GET", "/check", router.CheckHandler),
339+ router.NewRoute("GET", "/_metrics", promhttp.Handler().ServeHTTP),
340+ }
341+
342+ routes = append(
343+ routes,
344+ staticRoutes...,
345+ )
346+
347+ routes = append(
348+ routes,
349+ router.NewRoute("GET", "/([^/]+)", blogHandler),
350+ router.NewRoute("GET", "/([^/]+)/([^/]+)", postHandler),
351+ router.NewRoute("GET", "/([^/]+)/([^/]+)/raw", postHandlerRaw),
352+ router.NewRoute("GET", "/raw/([^/]+)/([^/]+)", postHandlerRaw),
353+ )
354+
355+ return routes
356+}
357+
358+func createSubdomainRoutes(staticRoutes []router.Route) []router.Route {
359+ routes := []router.Route{
360+ router.NewRoute("GET", "/", blogHandler),
361+ }
362+
363+ routes = append(
364+ routes,
365+ staticRoutes...,
366+ )
367+
368+ routes = append(
369+ routes,
370+ router.NewRoute("GET", "/([^/]+)", postHandler),
371+ router.NewRoute("GET", "/([^/]+)/raw", postHandlerRaw),
372+ router.NewRoute("GET", "/raw/([^/]+)", postHandlerRaw),
373+ )
374+
375+ return routes
376+}
377+
378+func StartApiServer() {
379+ cfg := NewConfigSite("pastes-web")
380+ db := postgres.NewDB(cfg.DbURL, cfg.Logger)
381+ defer func() {
382+ _ = db.Close()
383+ }()
384+ logger := cfg.Logger
385+
386+ go CronDeleteExpiredPosts(cfg, db)
387+
388+ staticRoutes := createStaticRoutes()
389+
390+ if cfg.Debug {
391+ staticRoutes = router.CreatePProfRoutes(staticRoutes)
392+ }
393+
394+ mainRoutes := createMainRoutes(staticRoutes)
395+ subdomainRoutes := createSubdomainRoutes(staticRoutes)
396+
397+ apiConfig := &router.ApiConfig{
398+ Cfg: cfg,
399+ Dbpool: db,
400+ }
401+ handler := router.CreateServe(mainRoutes, subdomainRoutes, apiConfig)
402+ router := http.HandlerFunc(handler)
403+
404+ portStr := fmt.Sprintf(":%s", cfg.Port)
405+ logger.Info(
406+ "Starting server on port",
407+ "port", cfg.Port,
408+ "domain", cfg.Domain,
409+ )
410+
411+ logger.Error(http.ListenAndServe(portStr, router).Error())
412+}
+27, -0
......@@ -0,0 +1,27 @@
1+package pastes
2+
3+import (
4+ "strings"
5+
6+ "github.com/picosh/pico/pkg/shared"
7+)
8+
9+func NewConfigSite(service string) *shared.ConfigSite {
10+ debug := shared.GetEnv("PASTES_DEBUG", "0")
11+ domain := shared.GetEnv("PASTES_DOMAIN", "pastes.sh")
12+ port := shared.GetEnv("PASTES_WEB_PORT", "3000")
13+ dbURL := shared.GetEnv("DATABASE_URL", "")
14+ protocol := shared.GetEnv("PASTES_PROTOCOL", "https")
15+ withPipe := strings.ToLower(shared.GetEnv("PICO_PIPE_ENABLED", "true")) == "true"
16+
17+ return &shared.ConfigSite{
18+ Debug: debug == "1",
19+ Domain: domain,
20+ Port: port,
21+ Protocol: protocol,
22+ DbURL: dbURL,
23+ Space: "pastes",
24+ Logger: shared.CreateLogger(service, withPipe),
25+ MaxAssetSize: int64(3 * shared.MB),
26+ }
27+}
+39, -0
......@@ -0,0 +1,39 @@
1+package pastes
2+
3+import (
4+ "time"
5+
6+ "github.com/picosh/pico/pkg/db"
7+ "github.com/picosh/pico/pkg/shared"
8+)
9+
10+func deleteExpiredPosts(cfg *shared.ConfigSite, dbpool db.DB) error {
11+ cfg.Logger.Info("checking for expired posts")
12+ posts, err := dbpool.FindExpiredPosts(cfg.Space)
13+ if err != nil {
14+ return err
15+ }
16+
17+ postIds := []string{}
18+ for _, post := range posts {
19+ postIds = append(postIds, post.ID)
20+ }
21+
22+ cfg.Logger.Info("deleting expired posts", "len", len(postIds))
23+ err = dbpool.RemovePosts(postIds)
24+ if err != nil {
25+ return err
26+ }
27+
28+ return nil
29+}
30+
31+func CronDeleteExpiredPosts(cfg *shared.ConfigSite, dbpool db.DB) {
32+ for {
33+ err := deleteExpiredPosts(cfg, dbpool)
34+ if err != nil {
35+ cfg.Logger.Error(err.Error())
36+ }
37+ time.Sleep(12 * time.Hour)
38+ }
39+}
+19, -0
......@@ -0,0 +1,19 @@
1+{{define "base"}}
2+<!doctype html>
3+<html lang="en">
4+ <head>
5+ <meta charset='utf-8'>
6+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7+ <title>{{template "title" .}}</title>
8+
9+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
10+
11+ <meta name="keywords" content="pastebin, paste, copy, snippets" />
12+ {{template "meta" .}}
13+
14+ <link rel="stylesheet" href="/smol.css" />
15+ <link rel="stylesheet" href="/main.css" />
16+ </head>
17+ <body {{template "attrs" .}}>{{template "body" .}}</body>
18+</html>
19+{{end}}
+46, -0
......@@ -0,0 +1,46 @@
1+{{template "base" .}}
2+
3+{{define "title"}}{{.PageTitle}}{{end}}
4+
5+{{define "meta"}}
6+<meta name="description" content="{{if .Header.Bio}}{{.Header.Bio}}{{else}}{{.Header.Title}}{{end}}" />
7+
8+<meta property="og:type" content="website">
9+<meta property="og:site_name" content="{{.Site.Domain}}">
10+<meta property="og:url" content="{{.URL}}">
11+<meta property="og:title" content="{{.Header.Title}}">
12+{{if .Header.Bio}}<meta property="og:description" content="{{.Header.Bio}}">{{end}}
13+<meta property="og:image:width" content="300" />
14+<meta property="og:image:height" content="300" />
15+<meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
16+<meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
17+
18+<meta property="twitter:card" content="summary">
19+<meta property="twitter:url" content="{{.URL}}">
20+<meta property="twitter:title" content="{{.Header.Title}}">
21+{{if .Header.Bio}}<meta property="twitter:description" content="{{.Header.Bio}}">{{end}}
22+<meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
23+<meta name="twitter:image:src" content="https://{{.Site.Domain}}/card.png" />
24+{{end}}
25+
26+{{define "attrs"}}{{end}}
27+
28+{{define "body"}}
29+<header class="text-center">
30+ <h1 class="text-2xl">{{.Header.Title}}</h1>
31+ <hr class="my-2" />
32+</header>
33+<main>
34+ <section class="posts">
35+ {{range .Posts}}
36+ <article class="my">
37+ <div class="flex items-center">
38+ <time datetime="{{.PublishAtISO}}" class="font-italic text-sm post-date">{{.PublishAt}}</time>
39+ <div class="text-md flex-1 transform-none m-0"><a href="{{.URL}}">{{.Title}}</a></div>
40+ </div>
41+ </article>
42+ {{end}}
43+ </section>
44+</main>
45+{{template "footer" .}}
46+{{end}}
+6, -0
......@@ -0,0 +1,6 @@
1+{{define "footer"}}
2+<footer>
3+ <hr class="my-2" />
4+ published with <a href={{.Site.HomeURL}}>{{.Site.Domain}}</a>
5+</footer>
6+{{end}}
+6, -0
......@@ -0,0 +1,6 @@
1+{{define "marketing-footer"}}
2+<footer>
3+ <hr />
4+ <p class="font-italic">Built and maintained by <a href="https://pico.sh">pico.sh</a>.</p>
5+</footer>
6+{{end}}
+39, -0
......@@ -0,0 +1,39 @@
1+{{template "base" .}}
2+
3+{{define "title"}}{{.Site.Domain}} -- a pastebin for hackers{{end}}
4+
5+{{define "meta"}}
6+<meta name="description" content="a pastebin for hackers" />
7+
8+<meta property="og:type" content="website">
9+<meta property="og:site_name" content="{{.Site.Domain}}">
10+<meta property="og:url" content="https://{{.Site.Domain}}">
11+<meta property="og:title" content="{{.Site.Domain}}">
12+<meta property="og:description" content="a pastebin for hackers">
13+
14+<meta name="twitter:card" content="summary" />
15+<meta property="twitter:url" content="https://{{.Site.Domain}}">
16+<meta property="twitter:title" content="{{.Site.Domain}}">
17+<meta property="twitter:description" content="a pastebin for hackers">
18+<meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
19+<meta name="twitter:image:src" content="https://{{.Site.Domain}}/card.png" />
20+
21+<meta property="og:image:width" content="300" />
22+<meta property="og:image:height" content="300" />
23+<meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
24+<meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
25+{{end}}
26+
27+{{define "attrs"}}{{end}}
28+
29+{{define "body"}}
30+<header class="text-center">
31+ <h1 class="text-2xl font-bold">{{.Site.Domain}}</h1>
32+ <p class="text-lg">a pastebin for hackers</p>
33+ <div>
34+ <a href="https://pico.sh/pastes" class="btn-link mt inline-block">LEARN MORE</a>
35+ </div>
36+</header>
37+
38+{{template "marketing-footer" .}}
39+{{end}}
+62, -0
......@@ -0,0 +1,62 @@
1+{{template "base" .}}
2+
3+{{define "title"}}{{.PageTitle}}{{end}}
4+
5+{{define "meta"}}
6+<meta property="og:type" content="website">
7+<meta property="og:site_name" content="{{.Site.Domain}}">
8+<meta property="og:url" content="{{.URL}}">
9+<meta property="og:title" content="{{.Title}}">
10+<meta property="og:image:width" content="300" />
11+<meta property="og:image:height" content="300" />
12+<meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
13+<meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
14+
15+<meta property="twitter:card" content="summary">
16+<meta property="twitter:url" content="{{.URL}}">
17+<meta property="twitter:title" content="{{.Title}}">
18+<meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
19+<meta name="twitter:image:src" content="https://{{.Site.Domain}}/card.png" />
20+
21+<link rel="stylesheet" href="/syntax.css" />
22+{{end}}
23+
24+{{define "attrs"}}id="post"{{end}}
25+
26+{{define "body"}}
27+<header>
28+ <h1 class="text-2xl font-bold transform-none">{{.Title}}</h1>
29+ <p class="font-bold m-0">
30+ {{if .Unlisted}} <code>unlisted</code>{{end}}
31+ <time datetime="{{.PublishAtISO}}">{{.PublishAt}}</time>
32+ <span> &middot; <span>
33+ <a href="{{.BlogURL}}">{{.BlogName}}</a>
34+ <span> &middot; <span>
35+ <a href="{{.RawURL}}">raw</a>
36+ </p>
37+ <p class="font-bold m-0">expires: {{.ExpiresAt}}</p>
38+ <script>
39+ function init() {
40+ const btn = document.querySelector("#copy-btn");
41+ const res = document.querySelector("#copy-result");
42+ const text = "{{.Text}}";
43+ btn.addEventListener("click", () => {
44+ res.innerHtml = "";
45+ navigator.clipboard.writeText(text)
46+ .then(() => res.innerHTML = "success!");
47+ })
48+ }
49+ document.addEventListener("DOMContentLoaded", init);
50+ </script>
51+</header>
52+<main>
53+ <div class="flex gap items-center">
54+ <button id="copy-btn" class="btn" style="background-color: transparent;">copy</button>
55+ <div id="copy-result" class=""></div>
56+ </div>
57+ <article>
58+ {{.Contents}}
59+ </article>
60+</main>
61+{{template "footer" .}}
62+{{end}}
+34, -0
......@@ -0,0 +1,34 @@
1+package pastes
2+
3+import (
4+ "bytes"
5+
6+ "github.com/alecthomas/chroma/v2/formatters/html"
7+ "github.com/alecthomas/chroma/v2/lexers"
8+ "github.com/alecthomas/chroma/v2/styles"
9+)
10+
11+func ParseText(filename string, text string) (string, error) {
12+ formatter := html.New(
13+ html.WithLineNumbers(true),
14+ html.WithLinkableLineNumbers(true, ""),
15+ html.WithClasses(true),
16+ )
17+ lexer := lexers.Match(filename)
18+ if lexer == nil {
19+ lexer = lexers.Analyse(text)
20+ }
21+ if lexer == nil {
22+ lexer = lexers.Get("plaintext")
23+ }
24+ iterator, err := lexer.Tokenise(nil, text)
25+ if err != nil {
26+ return text, err
27+ }
28+ var buf bytes.Buffer
29+ err = formatter.Format(&buf, styles.Get("dracula"), iterator)
30+ if err != nil {
31+ return text, err
32+ }
33+ return buf.String(), nil
34+}
+0, -0
Binaries are not rendered as diffs.
+0, -0
Binaries are not rendered as diffs.
+0, -0
Binaries are not rendered as diffs.
+0, -0
Binaries are not rendered as diffs.
+11, -0
......@@ -0,0 +1,11 @@
1+body {
2+ max-width: 1025px;
3+}
4+
5+.post-date {
6+ width: 110px;
7+}
8+
9+.transform-none {
10+ text-transform: none;
11+}
+2, -0
......@@ -0,0 +1,2 @@
1+User-agent: *
2+Allow: /
+875, -0
......@@ -0,0 +1,875 @@
1+*,
2+::before,
3+::after {
4+ box-sizing: border-box;
5+}
6+
7+::-moz-focus-inner {
8+ border-style: none;
9+ padding: 0;
10+}
11+:-moz-focusring {
12+ outline: 1px dotted ButtonText;
13+}
14+:-moz-ui-invalid {
15+ box-shadow: none;
16+}
17+
18+:root {
19+ --line-height: 1.3rem;
20+ --grid-height: 0.65rem;
21+ --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
22+}
23+
24+@media (prefers-color-scheme: light) {
25+ :root {
26+ --white: #2e3f53;
27+ --white-light: #cfe0f4;
28+ --white-dark: #6c6a6a;
29+ --code: #52576f;
30+ --pre: #e1e7ee;
31+ --bg-color: #f4f4f4;
32+ --text-color: #24292f;
33+ --link-color: #005cc5;
34+ --visited: #6f42c1;
35+ --blockquote: #005cc5;
36+ --blockquote-bg: #cfe0f4;
37+ --hover: #c11e7a;
38+ --grey: #ccc;
39+ --grey-light: #6a708e;
40+ --green: #1a7f37;
41+ }
42+}
43+
44+@media (prefers-color-scheme: dark) {
45+ :root {
46+ --white: #f2f2f2;
47+ --white-light: #f2f2f2;
48+ --white-dark: #e8e8e8;
49+ --code: #414558;
50+ --pre: #252525;
51+ --bg-color: #282a36;
52+ --text-color: #f2f2f2;
53+ --link-color: #8be9fd;
54+ --visited: #bd93f9;
55+ --blockquote: #bd93f9;
56+ --blockquote-bg: #353548;
57+ --hover: #ff80bf;
58+ --grey: #414558;
59+ --grey-light: #6a708e;
60+ --green: #50fa7b;
61+ }
62+}
63+
64+html {
65+ background-color: var(--bg-color);
66+ color: var(--text-color);
67+ line-height: var(--line-height);
68+ font-family:
69+ -apple-system,
70+ BlinkMacSystemFont,
71+ "Segoe UI",
72+ Roboto,
73+ Oxygen,
74+ Ubuntu,
75+ Cantarell,
76+ "Fira Sans",
77+ "Droid Sans",
78+ "Helvetica Neue",
79+ Arial,
80+ sans-serif,
81+ "Apple Color Emoji",
82+ "Segoe UI Emoji",
83+ "Segoe UI Symbol";
84+ -webkit-text-size-adjust: 100%;
85+ -moz-tab-size: 4;
86+ -o-tab-size: 4;
87+ tab-size: 4;
88+}
89+
90+body {
91+ margin: 0 auto;
92+}
93+
94+img {
95+ max-width: 100%;
96+ height: auto;
97+}
98+
99+b,
100+strong {
101+ font-weight: bold;
102+}
103+
104+code,
105+kbd,
106+samp,
107+pre {
108+ font-family: var(--font-mono);
109+ font-variant-numeric: tabular-nums;
110+}
111+
112+code,
113+kbd,
114+samp {
115+ border: 2px solid var(--code);
116+}
117+
118+pre > code {
119+ background-color: inherit;
120+ padding: 0;
121+ border: none;
122+ border-radius: 0;
123+}
124+
125+code {
126+ border-radius: 0.3rem;
127+ padding: 0.025rem 0.3rem;
128+}
129+
130+pre {
131+ border-radius: 1px;
132+ padding: var(--line-height);
133+ overflow-x: auto;
134+ background-color: var(--pre) !important;
135+}
136+
137+small {
138+ font-size: 0.8rem;
139+}
140+
141+details {
142+ border: 2px solid var(--grey-light);
143+ padding: calc(var(--grid-height) - 2px) 1ch;
144+ margin-bottom: var(--grid-height);
145+}
146+
147+details[open] summary {
148+ margin-bottom: var(--grid-height);
149+}
150+
151+summary {
152+ display: list-item;
153+ cursor: pointer;
154+}
155+
156+h1,
157+h2,
158+h3,
159+h4,
160+h5,
161+h6 {
162+ margin: 0;
163+ padding: 0;
164+ border: 0;
165+ font-family: var(--font-mono);
166+ font-weight: bold;
167+}
168+
169+h1 {
170+ font-size: 1.6rem;
171+ line-height: calc(var(--line-height) * 1.5);
172+ margin-top: calc(var(--line-height) * 2.5);
173+ margin-bottom: var(--grid-height);
174+}
175+
176+h2 {
177+ font-size: 1.3rem;
178+ line-height: var(--line-height);
179+ margin-top: calc(var(--line-height) * 2);
180+ margin-bottom: var(--grid-height);
181+}
182+
183+h3 {
184+ font-size: 1.1rem;
185+ line-height: var(--line-height);
186+ margin-top: calc(var(--line-height) * 1.5);
187+ margin-bottom: var(--grid-height);
188+}
189+
190+h4,
191+h5,
192+h6 {
193+ font-size: 1rem;
194+ line-height: var(--line-height);
195+ margin-top: var(--line-height);
196+ margin-bottom: var(--grid-height);
197+ color: var(--white-dark);
198+}
199+
200+
201+path {
202+ fill: var(--text-color);
203+ stroke: var(--text-color);
204+}
205+
206+hr {
207+ color: inherit;
208+ border: 0;
209+ height: 1px;
210+ background: var(--grey-light);
211+ margin: calc(var(--grid-height) - 1px) auto;
212+}
213+
214+hr.dashed {
215+ border: none;
216+ border-top: 1px dashed var(--grey-light);
217+ background: none;
218+ height: 0;
219+ margin: calc(var(--grid-height) - 1px) auto;
220+}
221+
222+a {
223+ color: var(--link-color);
224+ text-decoration: underline;
225+ text-decoration-thickness: 1px;
226+ text-underline-offset: 2px;
227+ padding: 0 2px;
228+ border-radius: 1px;
229+ transition: background-color 0.1s ease, color 0.1s ease;
230+}
231+
232+a:hover {
233+ background-color: var(--link-color);
234+ color: var(--bg-color);
235+ text-decoration: none;
236+}
237+
238+a:visited {
239+ color: var(--visited);
240+}
241+
242+a:visited:hover {
243+ background-color: var(--visited);
244+ color: var(--bg-color);
245+ text-decoration: none;
246+}
247+
248+section {
249+ margin-bottom: 1.4rem;
250+}
251+
252+section:last-child {
253+ margin-bottom: 0;
254+}
255+
256+header {
257+ margin: 1rem auto;
258+}
259+
260+p {
261+ margin-top: var(--line-height);
262+ margin-bottom: var(--line-height);
263+}
264+
265+article {
266+ overflow-wrap: break-word;
267+}
268+
269+blockquote {
270+ border-left: 5px solid var(--blockquote);
271+ background-color: var(--blockquote-bg);
272+ padding: var(--grid-height);
273+ margin: var(--line-height) 0;
274+}
275+
276+blockquote > p {
277+ margin: 0;
278+}
279+
280+blockquote code {
281+ border: 1px solid var(--blockquote);
282+}
283+
284+ul,
285+ol {
286+ padding: 0 15px;
287+ list-style-position: inside;
288+ list-style-type: square;
289+ margin: var(--grid-height) 0;
290+}
291+
292+ul[style*="list-style-type: none;"] {
293+ padding: 0;
294+}
295+
296+ol ul, ol ol, ul ol, ul ul {
297+ padding: 0 0 0 var(--line-height);
298+ margin: 0;
299+}
300+
301+li {
302+ margin: var(--grid-height) 0;
303+ padding: 0;
304+}
305+
306+li::marker {
307+ line-height: 0;
308+ color: var(--grey-light);
309+}
310+
311+li blockquote,
312+li pre {
313+ display: inline-block;
314+ margin: 0;
315+ width: 100%;
316+}
317+
318+li pre {
319+ padding: var(--grid-height);
320+}
321+
322+[role="list"] {
323+ display: flex;
324+ flex-direction: column;
325+ gap: var(--grid-height);
326+}
327+
328+[role="list"] [role="list"] {
329+ margin-left: 1.3rem;
330+}
331+
332+[role="listitem"] {
333+ display: flex;
334+ align-items: flex-start;
335+ gap: 5px;
336+}
337+
338+[role="listitem"]:has(pre, img, blockquote) {
339+ align-items: center;
340+}
341+
342+table {
343+ border-collapse: collapse;
344+ margin: var(--line-height) 0;
345+}
346+
347+th,
348+td {
349+ border: 1px solid var(--white);
350+ padding: var(--grid-height);
351+}
352+
353+footer {
354+ text-align: center;
355+ margin-bottom: calc(var(--line-height) * 3);
356+}
357+
358+dt {
359+ font-weight: bold;
360+}
361+
362+dd {
363+ margin-left: 0;
364+}
365+
366+dd:not(:last-child) {
367+ margin-bottom: 0.5rem;
368+}
369+
370+figure {
371+ margin: 0;
372+}
373+
374+sup {
375+ line-height: 0;
376+}
377+
378+#toc {
379+ margin-top: var(--line-height);
380+}
381+
382+.container {
383+ max-width: 50em;
384+ width: 100%;
385+}
386+
387+.container-sm {
388+ max-width: 40em;
389+ width: 100%;
390+}
391+
392+.mono,
393+time,
394+nav {
395+ font-family: var(--font-mono);
396+ font-variant-numeric: tabular-nums;
397+}
398+
399+.link-alt-hover,
400+.link-alt-hover:visited,
401+.link-alt-hover:visited:hover,
402+.link-alt-hover:hover {
403+ color: var(--hover);
404+ text-decoration: none;
405+}
406+
407+.link-alt-hover:visited:hover,
408+.link-alt-hover:hover {
409+ background-color: transparent;
410+ text-decoration: underline;
411+}
412+
413+.link-alt,
414+.link-alt:visited,
415+.link-alt:visited:hover,
416+.link-alt:hover {
417+ color: var(--white);
418+ text-decoration: none;
419+}
420+
421+.link-alt:visited:hover,
422+.link-alt:hover {
423+ background-color: transparent;
424+ text-decoration: underline;
425+}
426+
427+.link-grey,
428+.link-grey:visited {
429+ color: var(--grey-light);
430+ text-decoration: none;
431+}
432+
433+.link-grey:hover,
434+.link-grey:visited:hover {
435+ background-color: transparent;
436+ color: var(--text-color);
437+ text-decoration: underline;
438+}
439+
440+.text-2xl code, .text-xl code, .text-lg code, .text-md code {
441+ text-transform: none;
442+}
443+
444+.text-2xl {
445+ font-size: 1.6rem;
446+ font-weight: bold;
447+ line-height: calc(var(--line-height) * 1.5);
448+ margin-bottom: var(--grid-height);
449+}
450+
451+.text-xl {
452+ font-size: 1.3rem;
453+ font-weight: bold;
454+ line-height: var(--line-height);
455+ margin-bottom: var(--grid-height);
456+}
457+
458+.text-lg {
459+ font-size: 1.1rem;
460+ font-weight: bold;
461+ line-height: var(--line-height);
462+ margin-bottom: var(--grid-height);
463+}
464+
465+.text-md {
466+ font-size: 1rem;
467+ font-weight: bold;
468+ line-height: var(--line-height);
469+ margin-bottom: var(--grid-height);
470+}
471+
472+.text-right {
473+ text-align: right;
474+}
475+
476+.text-sm {
477+ font-size: 0.8rem;
478+}
479+
480+.w-full {
481+ width: 100%;
482+}
483+
484+.border {
485+ border: 2px solid var(--grey-light);
486+}
487+
488+.text-left {
489+ text-align: left;
490+}
491+
492+.text-center {
493+ text-align: center;
494+}
495+
496+.text-underline {
497+ text-decoration: underline;
498+ text-decoration-thickness: 2px;
499+}
500+
501+.text-hdr {
502+ color: var(--hover);
503+}
504+
505+.font-bold {
506+ font-weight: bold;
507+}
508+
509+.font-italic {
510+ font-style: italic;
511+}
512+
513+.inline {
514+ display: inline;
515+}
516+
517+.inline-block {
518+ display: inline-block;
519+}
520+
521+.max-w-half {
522+ max-width: 50%;
523+}
524+
525+.flex {
526+ display: flex;
527+}
528+
529+.flex-col {
530+ flex-direction: column;
531+}
532+
533+.flex-wrap {
534+ flex-wrap: wrap;
535+}
536+
537+.items-center {
538+ align-items: center;
539+}
540+
541+.m-0 {
542+ margin: 0;
543+}
544+
545+.mt-0 {
546+ margin-top: 0;
547+}
548+
549+.mt {
550+ margin-top: var(--grid-height);
551+}
552+
553+.mt-2 {
554+ margin-top: var(--line-height);
555+}
556+
557+.mt-4 {
558+ margin-top: calc(var(--line-height) * 2);
559+}
560+
561+.mb {
562+ margin-bottom: var(--grid-height);
563+}
564+
565+.mb-2 {
566+ margin-bottom: var(--line-height);
567+}
568+
569+.mb-4 {
570+ margin-bottom: calc(var(--line-height) * 2);
571+}
572+
573+.mr {
574+ margin-right: 0.5rem;
575+}
576+
577+.ml {
578+ margin-left: 0.5rem;
579+}
580+
581+.my {
582+ margin-top: var(--grid-height);
583+ margin-bottom: var(--grid-height);
584+}
585+
586+.my-2 {
587+ margin-top: var(--line-height);
588+ margin-bottom: var(--line-height);
589+}
590+
591+.my-4 {
592+ margin-top: calc(var(--line-height) * 2);
593+ margin-bottom: calc(var(--line-height) * 2);
594+}
595+
596+.p-0 {
597+ padding: 0;
598+}
599+
600+.px {
601+ padding-left: 0.5rem;
602+ padding-right: 0.5rem;
603+}
604+
605+.px-4 {
606+ padding-left: 2rem;
607+ padding-right: 2rem;
608+}
609+
610+.py {
611+ padding-top: var(--grid-height);
612+ padding-bottom: var(--grid-height);
613+}
614+
615+.py-4 {
616+ padding-top: calc(var(--line-height) * 2);
617+ padding-bottom: calc(var(--line-height) * 2);
618+}
619+
620+.justify-between {
621+ justify-content: space-between;
622+}
623+
624+.justify-center {
625+ justify-content: center;
626+}
627+
628+.gap {
629+ gap: var(--grid-height);
630+}
631+
632+.gap-2 {
633+ gap: var(--line-height);
634+}
635+
636+.group {
637+ display: flex;
638+ flex-direction: column;
639+ gap: var(--grid-height);
640+}
641+
642+.group-2 {
643+ display: flex;
644+ flex-direction: column;
645+ gap: var(--line-height);
646+}
647+
648+.group-h {
649+ display: flex;
650+ gap: var(--grid-height);
651+ align-items: center;
652+}
653+
654+.flex-1 {
655+ flex: 1;
656+}
657+
658+.items-end {
659+ align-items: end;
660+}
661+
662+.items-start {
663+ align-items: start;
664+}
665+
666+.justify-end {
667+ justify-content: end;
668+}
669+
670+.font-grey-light {
671+ color: var(--grey-light);
672+}
673+
674+.hidden {
675+ display: none;
676+}
677+
678+.align-right {
679+ text-align: right;
680+}
681+
682+.text-transform-none {
683+ text-transform: none;
684+}
685+
686+/* ==== MARKDOWN ==== */
687+
688+.md > :first-child {
689+ margin-top: 0 !important;
690+}
691+
692+.md h1,
693+.md h2,
694+.md h3,
695+.md h4 {
696+ padding: 0;
697+ font-weight: bold;
698+}
699+
700+.md h1 a,
701+.md h2 a,
702+.md h3 a,
703+.md h4 a {
704+ color: var(--grey-light);
705+ text-decoration: none;
706+}
707+
708+.md h1 a:hover,
709+.md h2 a:hover,
710+.md h3 a:hover,
711+.md h4 a:hover {
712+ background-color: transparent;
713+ color: var(--hover);
714+ text-decoration: underline;
715+}
716+
717+h1 code, h2 code, h3 code, h4 code {
718+ text-transform: none;
719+}
720+
721+/* ==== HELPERS ==== */
722+
723+.logo-header {
724+ line-height: 1;
725+ display: inline-block;
726+ background-color: #ff79c6;
727+ background-image: linear-gradient(to right, #ff5555, #ff79c6, #f8f859);
728+ color: transparent;
729+ background-clip: text;
730+ border: 3px solid #ff79c6;
731+ padding: 8px 10px 10px 10px;
732+ border-radius: 10px;
733+ background-size: 100%;
734+ margin: 0;
735+ -webkit-background-clip: text;
736+ -moz-background-clip: text;
737+ -webkit-text-fill-color: transparent;
738+ -moz-text-fill-color: transparent;
739+}
740+
741+.btn {
742+ border: 2px solid var(--link-color);
743+ color: var(--link-color);
744+ padding: 0.4rem 1rem;
745+ font-weight: bold;
746+ display: inline-block;
747+}
748+
749+.btn:hover {
750+ background-color: var(--link-color);
751+ color: var(--bg-color);
752+}
753+
754+.btn-link,
755+.btn-link:visited {
756+ border: 2px solid var(--link-color);
757+ color: var(--link-color);
758+ padding: var(--grid-height);
759+ text-decoration: none;
760+ font-weight: bold;
761+ display: inline-block;
762+}
763+
764+.btn-link:hover,
765+.btn-link:visited:hover {
766+ background-color: var(--link-color);
767+ color: var(--bg-color);
768+ text-decoration: none;
769+}
770+
771+.box {
772+ border: 1px solid var(--grey-light);
773+ padding: var(--grid-height);
774+ background-color: var(--pre);
775+}
776+
777+.box-sm {
778+ border: 1px solid var(--grey-light);
779+ padding: var(--grid-height);
780+ background-color: var(--pre);
781+}
782+
783+.box-dashed {
784+ border: 1px dashed var(--grey-light);
785+ padding: var(--grid-height);
786+}
787+
788+.box-alert {
789+ border: 1px solid var(--hover);
790+ padding: var(--line-height);
791+}
792+
793+.box-sm-alert {
794+ border: 1px solid var(--hover);
795+ padding: var(--grid-height);
796+}
797+
798+.text-green {
799+ color: var(--green);
800+}
801+
802+.prompt,
803+pre.cmd::before {
804+ content: "$ ";
805+ color: var(--green);
806+ font-weight: bold;
807+}
808+
809+.list-none {
810+ list-style-type: none;
811+}
812+
813+.list-square {
814+ list-style-type: square;
815+}
816+
817+.list-disc {
818+ list-style-type: disc;
819+}
820+
821+.list-decimal {
822+ list-style-type: decimal;
823+}
824+
825+.pill {
826+ border: 1px solid var(--link-color);
827+ color: var(--link-color);
828+}
829+
830+.pill-alert {
831+ border: 1px solid var(--hover);
832+ color: var(--hover);
833+}
834+
835+.pill-info {
836+ border: 1px solid var(--visited);
837+ color: var(--visited);
838+}
839+
840+@media only screen and (max-width: 40em) {
841+ body {
842+ padding: 0 var(--grid-height);
843+ }
844+
845+ header {
846+ margin: 0;
847+ }
848+
849+ .flex-collapse {
850+ flex-direction: column;
851+ }
852+}
853+
854+body {
855+ position: relative;
856+}
857+
858+/*
859+.debug-grid {
860+ width: 100%;
861+ height: 100%;
862+ position: absolute;
863+ top: 0;
864+ left: 0;
865+ right: 0;
866+ bottom: 0;
867+ z-index: 1000;
868+ pointer-events: none;
869+ background-image:
870+ repeating-linear-gradient(rgba(255, 121, 198, 0.18) 0 1px, transparent 1px 100%),
871+ repeating-linear-gradient(90deg, rgba(139, 233, 253, 0.10) 0 1px, transparent 1px 100%);
872+ background-size: 1ch var(--grid-height);
873+ margin: 0;
874+}
875+*/
+700, -0
......@@ -0,0 +1,700 @@
1+@media (prefers-color-scheme: light) {
2+ /* Background */
3+ .bg {
4+ background-color: #ffffff;
5+ }
6+ /* PreWrapper */
7+ .chroma {
8+ background-color: #ffffff;
9+ }
10+ /* Other */
11+ .chroma .x {
12+ }
13+ /* Error */
14+ .chroma .err {
15+ background-color: #a848a8;
16+ }
17+ /* CodeLine */
18+ .chroma .cl {
19+ }
20+ /* LineTableTD */
21+ .chroma .lntd {
22+ vertical-align: top;
23+ padding: 0;
24+ margin: 0;
25+ border: 0;
26+ }
27+ /* LineTable */
28+ .chroma .lntable {
29+ border-spacing: 0;
30+ padding: 0;
31+ margin: 0;
32+ border: 0;
33+ }
34+ /* LineHighlight */
35+ .chroma .hl {
36+ background-color: #ffffcc;
37+ }
38+ /* LineNumbersTable */
39+ .chroma .lnt {
40+ white-space: pre;
41+ user-select: none;
42+ margin-right: 0.4em;
43+ padding: 0 0.4em 0 0.4em;
44+ color: #7f7f7f;
45+ }
46+ /* LineNumbers */
47+ .chroma .ln {
48+ white-space: pre;
49+ user-select: none;
50+ margin-right: 0.4em;
51+ padding: 0 0.4em 0 0.4em;
52+ color: #7f7f7f;
53+ }
54+ /* Line */
55+ .chroma .line {
56+ display: flex;
57+ }
58+ /* Keyword */
59+ .chroma .k {
60+ color: #2838b0;
61+ }
62+ /* KeywordConstant */
63+ .chroma .kc {
64+ color: #444444;
65+ font-style: italic;
66+ }
67+ /* KeywordDeclaration */
68+ .chroma .kd {
69+ color: #2838b0;
70+ font-style: italic;
71+ }
72+ /* KeywordNamespace */
73+ .chroma .kn {
74+ color: #2838b0;
75+ }
76+ /* KeywordPseudo */
77+ .chroma .kp {
78+ color: #2838b0;
79+ }
80+ /* KeywordReserved */
81+ .chroma .kr {
82+ color: #2838b0;
83+ }
84+ /* KeywordType */
85+ .chroma .kt {
86+ color: #2838b0;
87+ font-style: italic;
88+ }
89+ /* Name */
90+ .chroma .n {
91+ }
92+ /* NameAttribute */
93+ .chroma .na {
94+ color: #388038;
95+ }
96+ /* NameBuiltin */
97+ .chroma .nb {
98+ color: #388038;
99+ }
100+ /* NameBuiltinPseudo */
101+ .chroma .bp {
102+ font-style: italic;
103+ }
104+ /* NameClass */
105+ .chroma .nc {
106+ color: #287088;
107+ }
108+ /* NameConstant */
109+ .chroma .no {
110+ color: #b85820;
111+ }
112+ /* NameDecorator */
113+ .chroma .nd {
114+ color: #287088;
115+ }
116+ /* NameEntity */
117+ .chroma .ni {
118+ color: #709030;
119+ }
120+ /* NameException */
121+ .chroma .ne {
122+ color: #908828;
123+ }
124+ /* NameFunction */
125+ .chroma .nf {
126+ color: #785840;
127+ }
128+ /* NameFunctionMagic */
129+ .chroma .fm {
130+ color: #b85820;
131+ }
132+ /* NameLabel */
133+ .chroma .nl {
134+ color: #289870;
135+ }
136+ /* NameNamespace */
137+ .chroma .nn {
138+ color: #289870;
139+ }
140+ /* NameOther */
141+ .chroma .nx {
142+ }
143+ /* NameProperty */
144+ .chroma .py {
145+ }
146+ /* NameTag */
147+ .chroma .nt {
148+ color: #2838b0;
149+ }
150+ /* NameVariable */
151+ .chroma .nv {
152+ color: #b04040;
153+ }
154+ /* NameVariableClass */
155+ .chroma .vc {
156+ }
157+ /* NameVariableGlobal */
158+ .chroma .vg {
159+ color: #908828;
160+ }
161+ /* NameVariableInstance */
162+ .chroma .vi {
163+ }
164+ /* NameVariableMagic */
165+ .chroma .vm {
166+ color: #b85820;
167+ }
168+ /* Literal */
169+ .chroma .l {
170+ }
171+ /* LiteralDate */
172+ .chroma .ld {
173+ }
174+ /* LiteralString */
175+ .chroma .s {
176+ color: #b83838;
177+ }
178+ /* LiteralStringAffix */
179+ .chroma .sa {
180+ color: #444444;
181+ }
182+ /* LiteralStringBacktick */
183+ .chroma .sb {
184+ color: #b83838;
185+ }
186+ /* LiteralStringChar */
187+ .chroma .sc {
188+ color: #a848a8;
189+ }
190+ /* LiteralStringDelimiter */
191+ .chroma .dl {
192+ color: #b85820;
193+ }
194+ /* LiteralStringDoc */
195+ .chroma .sd {
196+ color: #b85820;
197+ font-style: italic;
198+ }
199+ /* LiteralStringDouble */
200+ .chroma .s2 {
201+ color: #b83838;
202+ }
203+ /* LiteralStringEscape */
204+ .chroma .se {
205+ color: #709030;
206+ }
207+ /* LiteralStringHeredoc */
208+ .chroma .sh {
209+ color: #b83838;
210+ }
211+ /* LiteralStringInterpol */
212+ .chroma .si {
213+ color: #b83838;
214+ text-decoration: underline;
215+ }
216+ /* LiteralStringOther */
217+ .chroma .sx {
218+ color: #a848a8;
219+ }
220+ /* LiteralStringRegex */
221+ .chroma .sr {
222+ color: #a848a8;
223+ }
224+ /* LiteralStringSingle */
225+ .chroma .s1 {
226+ color: #b83838;
227+ }
228+ /* LiteralStringSymbol */
229+ .chroma .ss {
230+ color: #b83838;
231+ }
232+ /* LiteralNumber */
233+ .chroma .m {
234+ color: #444444;
235+ }
236+ /* LiteralNumberBin */
237+ .chroma .mb {
238+ color: #444444;
239+ }
240+ /* LiteralNumberFloat */
241+ .chroma .mf {
242+ color: #444444;
243+ }
244+ /* LiteralNumberHex */
245+ .chroma .mh {
246+ color: #444444;
247+ }
248+ /* LiteralNumberInteger */
249+ .chroma .mi {
250+ color: #444444;
251+ }
252+ /* LiteralNumberIntegerLong */
253+ .chroma .il {
254+ color: #444444;
255+ }
256+ /* LiteralNumberOct */
257+ .chroma .mo {
258+ color: #444444;
259+ }
260+ /* Operator */
261+ .chroma .o {
262+ color: #666666;
263+ }
264+ /* OperatorWord */
265+ .chroma .ow {
266+ color: #a848a8;
267+ }
268+ /* Punctuation */
269+ .chroma .p {
270+ color: #888888;
271+ }
272+ /* Comment */
273+ .chroma .c {
274+ color: #888888;
275+ font-style: italic;
276+ }
277+ /* CommentHashbang */
278+ .chroma .ch {
279+ color: #287088;
280+ font-style: italic;
281+ }
282+ /* CommentMultiline */
283+ .chroma .cm {
284+ color: #888888;
285+ font-style: italic;
286+ }
287+ /* CommentSingle */
288+ .chroma .c1 {
289+ color: #888888;
290+ font-style: italic;
291+ }
292+ /* CommentSpecial */
293+ .chroma .cs {
294+ color: #888888;
295+ font-style: italic;
296+ }
297+ /* CommentPreproc */
298+ .chroma .cp {
299+ color: #289870;
300+ }
301+ /* CommentPreprocFile */
302+ .chroma .cpf {
303+ color: #289870;
304+ }
305+ /* Generic */
306+ .chroma .g {
307+ }
308+ /* GenericDeleted */
309+ .chroma .gd {
310+ color: #c02828;
311+ }
312+ /* GenericEmph */
313+ .chroma .ge {
314+ font-style: italic;
315+ }
316+ /* GenericError */
317+ .chroma .gr {
318+ color: #c02828;
319+ }
320+ /* GenericHeading */
321+ .chroma .gh {
322+ color: #666666;
323+ }
324+ /* GenericInserted */
325+ .chroma .gi {
326+ color: #388038;
327+ }
328+ /* GenericOutput */
329+ .chroma .go {
330+ color: #666666;
331+ }
332+ /* GenericPrompt */
333+ .chroma .gp {
334+ color: #444444;
335+ }
336+ /* GenericStrong */
337+ .chroma .gs {
338+ font-weight: bold;
339+ }
340+ /* GenericSubheading */
341+ .chroma .gu {
342+ color: #444444;
343+ }
344+ /* GenericTraceback */
345+ .chroma .gt {
346+ color: #2838b0;
347+ }
348+ /* GenericUnderline */
349+ .chroma .gl {
350+ text-decoration: underline;
351+ }
352+ /* TextWhitespace */
353+ .chroma .w {
354+ color: #a89028;
355+ }
356+}
357+
358+@media (prefers-color-scheme: dark) {
359+ /* Background */
360+ .bg {
361+ color: #f8f8f2;
362+ background-color: #282a36;
363+ }
364+ /* PreWrapper */
365+ .chroma {
366+ color: #f8f8f2;
367+ background-color: #282a36;
368+ }
369+ /* Other */
370+ .chroma .x {
371+ }
372+ /* Error */
373+ .chroma .err {
374+ }
375+ /* CodeLine */
376+ .chroma .cl {
377+ }
378+ /* LineTableTD */
379+ .chroma .lntd {
380+ vertical-align: top;
381+ padding: 0;
382+ margin: 0;
383+ border: 0;
384+ }
385+ /* LineTable */
386+ .chroma .lntable {
387+ border-spacing: 0;
388+ padding: 0;
389+ margin: 0;
390+ border: 0;
391+ }
392+ /* LineHighlight */
393+ .chroma .hl {
394+ background-color: #ffffcc;
395+ }
396+ /* LineNumbersTable */
397+ .chroma .lnt {
398+ white-space: pre;
399+ user-select: none;
400+ margin-right: 0.4em;
401+ padding: 0 0.4em 0 0.4em;
402+ color: #7f7f7f;
403+ }
404+ /* LineNumbers */
405+ .chroma .ln {
406+ white-space: pre;
407+ user-select: none;
408+ margin-right: 0.4em;
409+ padding: 0 0.4em 0 0.4em;
410+ color: #7f7f7f;
411+ }
412+ /* Line */
413+ .chroma .line {
414+ display: flex;
415+ }
416+ /* Keyword */
417+ .chroma .k {
418+ color: #ff79c6;
419+ }
420+ /* KeywordConstant */
421+ .chroma .kc {
422+ color: #ff79c6;
423+ }
424+ /* KeywordDeclaration */
425+ .chroma .kd {
426+ color: #8be9fd;
427+ font-style: italic;
428+ }
429+ /* KeywordNamespace */
430+ .chroma .kn {
431+ color: #ff79c6;
432+ }
433+ /* KeywordPseudo */
434+ .chroma .kp {
435+ color: #ff79c6;
436+ }
437+ /* KeywordReserved */
438+ .chroma .kr {
439+ color: #ff79c6;
440+ }
441+ /* KeywordType */
442+ .chroma .kt {
443+ color: #8be9fd;
444+ }
445+ /* Name */
446+ .chroma .n {
447+ }
448+ /* NameAttribute */
449+ .chroma .na {
450+ color: #50fa7b;
451+ }
452+ /* NameBuiltin */
453+ .chroma .nb {
454+ color: #8be9fd;
455+ font-style: italic;
456+ }
457+ /* NameBuiltinPseudo */
458+ .chroma .bp {
459+ }
460+ /* NameClass */
461+ .chroma .nc {
462+ color: #50fa7b;
463+ }
464+ /* NameConstant */
465+ .chroma .no {
466+ }
467+ /* NameDecorator */
468+ .chroma .nd {
469+ }
470+ /* NameEntity */
471+ .chroma .ni {
472+ }
473+ /* NameException */
474+ .chroma .ne {
475+ }
476+ /* NameFunction */
477+ .chroma .nf {
478+ color: #50fa7b;
479+ }
480+ /* NameFunctionMagic */
481+ .chroma .fm {
482+ }
483+ /* NameLabel */
484+ .chroma .nl {
485+ color: #8be9fd;
486+ font-style: italic;
487+ }
488+ /* NameNamespace */
489+ .chroma .nn {
490+ }
491+ /* NameOther */
492+ .chroma .nx {
493+ }
494+ /* NameProperty */
495+ .chroma .py {
496+ }
497+ /* NameTag */
498+ .chroma .nt {
499+ color: #ff79c6;
500+ }
501+ /* NameVariable */
502+ .chroma .nv {
503+ color: #8be9fd;
504+ font-style: italic;
505+ }
506+ /* NameVariableClass */
507+ .chroma .vc {
508+ color: #8be9fd;
509+ font-style: italic;
510+ }
511+ /* NameVariableGlobal */
512+ .chroma .vg {
513+ color: #8be9fd;
514+ font-style: italic;
515+ }
516+ /* NameVariableInstance */
517+ .chroma .vi {
518+ color: #8be9fd;
519+ font-style: italic;
520+ }
521+ /* NameVariableMagic */
522+ .chroma .vm {
523+ }
524+ /* Literal */
525+ .chroma .l {
526+ }
527+ /* LiteralDate */
528+ .chroma .ld {
529+ }
530+ /* LiteralString */
531+ .chroma .s {
532+ color: #f1fa8c;
533+ }
534+ /* LiteralStringAffix */
535+ .chroma .sa {
536+ color: #f1fa8c;
537+ }
538+ /* LiteralStringBacktick */
539+ .chroma .sb {
540+ color: #f1fa8c;
541+ }
542+ /* LiteralStringChar */
543+ .chroma .sc {
544+ color: #f1fa8c;
545+ }
546+ /* LiteralStringDelimiter */
547+ .chroma .dl {
548+ color: #f1fa8c;
549+ }
550+ /* LiteralStringDoc */
551+ .chroma .sd {
552+ color: #f1fa8c;
553+ }
554+ /* LiteralStringDouble */
555+ .chroma .s2 {
556+ color: #f1fa8c;
557+ }
558+ /* LiteralStringEscape */
559+ .chroma .se {
560+ color: #f1fa8c;
561+ }
562+ /* LiteralStringHeredoc */
563+ .chroma .sh {
564+ color: #f1fa8c;
565+ }
566+ /* LiteralStringInterpol */
567+ .chroma .si {
568+ color: #f1fa8c;
569+ }
570+ /* LiteralStringOther */
571+ .chroma .sx {
572+ color: #f1fa8c;
573+ }
574+ /* LiteralStringRegex */
575+ .chroma .sr {
576+ color: #f1fa8c;
577+ }
578+ /* LiteralStringSingle */
579+ .chroma .s1 {
580+ color: #f1fa8c;
581+ }
582+ /* LiteralStringSymbol */
583+ .chroma .ss {
584+ color: #f1fa8c;
585+ }
586+ /* LiteralNumber */
587+ .chroma .m {
588+ color: #bd93f9;
589+ }
590+ /* LiteralNumberBin */
591+ .chroma .mb {
592+ color: #bd93f9;
593+ }
594+ /* LiteralNumberFloat */
595+ .chroma .mf {
596+ color: #bd93f9;
597+ }
598+ /* LiteralNumberHex */
599+ .chroma .mh {
600+ color: #bd93f9;
601+ }
602+ /* LiteralNumberInteger */
603+ .chroma .mi {
604+ color: #bd93f9;
605+ }
606+ /* LiteralNumberIntegerLong */
607+ .chroma .il {
608+ color: #bd93f9;
609+ }
610+ /* LiteralNumberOct */
611+ .chroma .mo {
612+ color: #bd93f9;
613+ }
614+ /* Operator */
615+ .chroma .o {
616+ color: #ff79c6;
617+ }
618+ /* OperatorWord */
619+ .chroma .ow {
620+ color: #ff79c6;
621+ }
622+ /* Punctuation */
623+ .chroma .p {
624+ }
625+ /* Comment */
626+ .chroma .c {
627+ color: #6272a4;
628+ }
629+ /* CommentHashbang */
630+ .chroma .ch {
631+ color: #6272a4;
632+ }
633+ /* CommentMultiline */
634+ .chroma .cm {
635+ color: #6272a4;
636+ }
637+ /* CommentSingle */
638+ .chroma .c1 {
639+ color: #6272a4;
640+ }
641+ /* CommentSpecial */
642+ .chroma .cs {
643+ color: #6272a4;
644+ }
645+ /* CommentPreproc */
646+ .chroma .cp {
647+ color: #ff79c6;
648+ }
649+ /* CommentPreprocFile */
650+ .chroma .cpf {
651+ color: #ff79c6;
652+ }
653+ /* Generic */
654+ .chroma .g {
655+ }
656+ /* GenericDeleted */
657+ .chroma .gd {
658+ color: #ff5555;
659+ }
660+ /* GenericEmph */
661+ .chroma .ge {
662+ text-decoration: underline;
663+ }
664+ /* GenericError */
665+ .chroma .gr {
666+ }
667+ /* GenericHeading */
668+ .chroma .gh {
669+ font-weight: bold;
670+ }
671+ /* GenericInserted */
672+ .chroma .gi {
673+ color: #50fa7b;
674+ font-weight: bold;
675+ }
676+ /* GenericOutput */
677+ .chroma .go {
678+ color: #44475a;
679+ }
680+ /* GenericPrompt */
681+ .chroma .gp {
682+ }
683+ /* GenericStrong */
684+ .chroma .gs {
685+ }
686+ /* GenericSubheading */
687+ .chroma .gu {
688+ font-weight: bold;
689+ }
690+ /* GenericTraceback */
691+ .chroma .gt {
692+ }
693+ /* GenericUnderline */
694+ .chroma .gl {
695+ text-decoration: underline;
696+ }
697+ /* TextWhitespace */
698+ .chroma .w {
699+ }
700+}
+107, -0
......@@ -0,0 +1,107 @@
1+package pastes
2+
3+import (
4+ "fmt"
5+ "strconv"
6+ "strings"
7+ "time"
8+
9+ "github.com/araddon/dateparse"
10+ "github.com/picosh/pico/pkg/db"
11+ "github.com/picosh/pico/pkg/filehandlers"
12+ "github.com/picosh/pico/pkg/pssh"
13+ "github.com/picosh/pico/pkg/shared"
14+)
15+
16+var DEFAULT_EXPIRES_AT = 90
17+
18+type FileHooks struct {
19+ Cfg *shared.ConfigSite
20+ Db db.DB
21+}
22+
23+func (p *FileHooks) FileValidate(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) (bool, error) {
24+ if !shared.IsTextFile(string(data.Text)) {
25+ err := fmt.Errorf(
26+ "ERROR: (%s) invalid file must be plain text (utf-8), skipping",
27+ data.Filename,
28+ )
29+ return false, err
30+ }
31+
32+ maxFileSize := int(p.Cfg.MaxAssetSize)
33+ if data.FileSize > maxFileSize {
34+ return false, fmt.Errorf(
35+ "ERROR: file (%s) has exceeded maximum file size (%d bytes)",
36+ data.Filename,
37+ maxFileSize,
38+ )
39+ }
40+
41+ return true, nil
42+}
43+
44+func (p *FileHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) error {
45+ data.Title = shared.ToUpper(data.Slug)
46+ // we want the slug to be the filename for pastes
47+ data.Slug = data.Filename
48+
49+ if data.ExpiresAt == nil || data.ExpiresAt.IsZero() {
50+ // mark posts for deletion a X days after creation
51+ expiresAt := time.Now().AddDate(0, 0, DEFAULT_EXPIRES_AT)
52+ data.ExpiresAt = &expiresAt
53+ }
54+
55+ var hidden bool
56+ var expiresFound bool
57+ var expires *time.Time
58+
59+ cmd := s.Command()
60+
61+ for _, arg := range cmd {
62+ if strings.Contains(arg, "=") {
63+ splitArg := strings.Split(arg, "=")
64+ if len(splitArg) != 2 {
65+ continue
66+ }
67+
68+ switch splitArg[0] {
69+ case "hidden":
70+ val, err := strconv.ParseBool(splitArg[1])
71+ if err != nil {
72+ continue
73+ }
74+
75+ hidden = val
76+ case "expires":
77+ val, err := strconv.ParseBool(splitArg[1])
78+ if err == nil {
79+ expiresFound = !val
80+ continue
81+ }
82+
83+ duration, err := time.ParseDuration(splitArg[1])
84+ if err == nil {
85+ expiresFound = true
86+ expireTime := time.Now().Add(duration)
87+ expires = &expireTime
88+ continue
89+ }
90+
91+ expireTime, err := dateparse.ParseStrict(splitArg[1])
92+ if err == nil {
93+ expiresFound = true
94+ expires = &expireTime
95+ }
96+ }
97+ }
98+ }
99+
100+ data.Hidden = hidden
101+
102+ if expiresFound {
103+ data.ExpiresAt = expires
104+ }
105+
106+ return nil
107+}
+97, -0
......@@ -0,0 +1,97 @@
1+package pastes
2+
3+import (
4+ "context"
5+ "os"
6+ "os/signal"
7+ "syscall"
8+ "time"
9+
10+ "github.com/picosh/pico/pkg/db/postgres"
11+ "github.com/picosh/pico/pkg/filehandlers"
12+ "github.com/picosh/pico/pkg/pssh"
13+ "github.com/picosh/pico/pkg/send/auth"
14+ "github.com/picosh/pico/pkg/send/list"
15+ "github.com/picosh/pico/pkg/send/pipe"
16+ "github.com/picosh/pico/pkg/send/protocols/rsync"
17+ "github.com/picosh/pico/pkg/send/protocols/scp"
18+ "github.com/picosh/pico/pkg/send/protocols/sftp"
19+ "github.com/picosh/pico/pkg/shared"
20+)
21+
22+func StartSshServer() {
23+ appName := "pastes-ssh"
24+
25+ host := shared.GetEnv("PASTES_HOST", "0.0.0.0")
26+ port := shared.GetEnv("PASTES_SSH_PORT", "2222")
27+ promPort := shared.GetEnv("PASTES_PROM_PORT", "9222")
28+ cfg := NewConfigSite(appName)
29+ logger := cfg.Logger
30+
31+ ctx, cancel := context.WithCancel(context.Background())
32+ defer cancel()
33+
34+ dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
35+ defer func() {
36+ _ = dbh.Close()
37+ }()
38+ hooks := &FileHooks{
39+ Cfg: cfg,
40+ Db: dbh,
41+ }
42+
43+ fileMap := map[string]filehandlers.ReadWriteHandler{
44+ "fallback": filehandlers.NewScpPostHandler(dbh, cfg, hooks),
45+ }
46+ handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
47+ sshAuth := shared.NewSshAuthHandler(dbh, logger, "pastes")
48+
49+ // Create a new SSH server
50+ server, err := pssh.NewSSHServerWithConfig(
51+ ctx,
52+ logger,
53+ appName,
54+ host,
55+ port,
56+ promPort,
57+ "ssh_data/term_info_ed25519",
58+ sshAuth.PubkeyAuthHandler,
59+ []pssh.SSHServerMiddleware{
60+ pipe.Middleware(handler, ""),
61+ list.Middleware(handler),
62+ scp.Middleware(handler),
63+ rsync.Middleware(handler),
64+ auth.Middleware(handler),
65+ pssh.PtyMdw(pssh.DeprecatedNotice(), 200*time.Millisecond),
66+ pssh.LogMiddleware(handler, dbh),
67+ },
68+ []pssh.SSHServerMiddleware{
69+ sftp.Middleware(handler),
70+ pssh.LogMiddleware(handler, dbh),
71+ },
72+ nil,
73+ )
74+
75+ if err != nil {
76+ logger.Error("failed to create ssh server", "err", err.Error())
77+ os.Exit(1)
78+ }
79+
80+ done := make(chan os.Signal, 1)
81+ signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
82+ logger.Info("Starting SSH server", "addr", server.Config.ListenAddr)
83+ go func() {
84+ if err = server.ListenAndServe(); err != nil {
85+ logger.Error("serve", "err", err.Error())
86+ os.Exit(1)
87+ }
88+ }()
89+
90+ exit := func() {
91+ logger.Info("stopping ssh server")
92+ cancel()
93+ }
94+
95+ <-done
96+ exit()
97+}
+4, -3
......@@ -385,9 +385,10 @@ func (m *ErrMultiplePublicKeys) Error() string {
385385 }
386386
387387 type UserStats struct {
388- Prose UserServiceStats
389- Feeds UserServiceStats
390- Pages UserServiceStats
388+ Prose UserServiceStats
389+ Pastes UserServiceStats
390+ Feeds UserServiceStats
391+ Pages UserServiceStats
391392 }
392393
393394 type UserServiceStats struct {
+2, -0
......@@ -2101,6 +2101,8 @@ func (me *PsqlDB) FindUserStats(userID string) (*db.UserStats, error) {
21012101 switch stat.Service {
21022102 case "prose":
21032103 stats.Prose = stat
2104+ case "pastes":
2105+ stats.Pastes = stat
21042106 case "feeds":
21052107 stats.Feeds = stat
21062108 }
+2, -2
......@@ -748,11 +748,11 @@ func TestFindExpiredPosts(t *testing.T) {
748748 Filename: "expired.txt",
749749 Slug: "expired",
750750 Title: "Expired",
751- Space: "prose",
751+ Space: "pastes",
752752 ExpiresAt: &expired,
753753 })
754754
755- posts, err := testDB.FindExpiredPosts("prose")
755+ posts, err := testDB.FindExpiredPosts("pastes")
756756 if err != nil {
757757 t.Fatalf("FindExpiredPosts failed: %v", err)
758758 }
+2, -1
......@@ -167,7 +167,7 @@ func (m *ServicesList) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
167167 brd := NewBorder(services)
168168 brd.Label = "services"
169169 brd.Style = vaxis.Style{Foreground: purp}
170- servicesHeight := 7
170+ servicesHeight := 8
171171 return brd.Draw(vxfw.DrawContext{
172172 Characters: ctx.Characters,
173173 Max: vxfw.Size{
......@@ -192,6 +192,7 @@ func (m *ServicesList) getServiceKv() []Kv {
192192 data := [][]string{
193193 {"name", "status"},
194194 {"pipe", "active"},
195+ {"pastes", "active"},
195196 {"prose", proseStatus},
196197 {"pages", pagesStatus},
197198 }
+2, -0
......@@ -127,10 +127,12 @@ func (m *MenuPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
127127 if m.stats != nil {
128128 pages, _ := NewUsageInfo("pages", &m.stats.Pages).Draw(ctx)
129129 prose, _ := NewUsageInfo("prose", &m.stats.Prose).Draw(ctx)
130+ pastes, _ := NewUsageInfo("pastes", &m.stats.Pastes).Draw(ctx)
130131 feeds, _ := NewUsageInfo("rss-to-email", &m.stats.Feeds).Draw(ctx)
131132 right = append(right,
132133 pages,
133134 prose,
135+ pastes,
134136 feeds,
135137 )
136138 }