aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--error.tpl22
-rw-r--r--go.mod5
-rw-r--r--go.sum2
-rw-r--r--index.tpl32
-rw-r--r--main.go235
-rw-r--r--readme.md18
-rw-r--r--style.css71
-rw-r--r--view.tpl21
9 files changed, 407 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..cc10a06
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+bpaste
diff --git a/error.tpl b/error.tpl
new file mode 100644
index 0000000..c53fa4f
--- /dev/null
+++ b/error.tpl
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <link rel="stylesheet" href="/style.css" defer>
+ <title>Error {{.Short}}</title>
+</head>
+<body>
+<nav>
+ <a href="/">Home</a>
+ <div style="display: block; float: right;">
+ </div>
+</nav>
+
+<h1>{{.Short}}</h1>
+
+{{.Long}}
+
+</body>
+</html>
+
+
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..f3a6dab
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,5 @@
+module riedstra.dev/mitch/bpaste
+
+go 1.16
+
+require github.com/gorilla/mux v1.8.0 // indirect
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..5350288
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,2 @@
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
diff --git a/index.tpl b/index.tpl
new file mode 100644
index 0000000..a117af9
--- /dev/null
+++ b/index.tpl
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <link rel="stylesheet" href="/style.css" defer>
+ <title>Brutally Simple Pastebin</title>
+</head>
+<body>
+<nav>
+ <a href="/">Home</a>
+ <div style="display: block; float: right;">
+ </div>
+</nav>
+
+<container>
+ <h1>Brutally simple pastebin</h1>
+
+ <form action="/new" method="POST" id="postform">
+ <label for="title">Title:</label>
+ <input id="title" name="title" type="text" />
+
+ <label for="content">Paste Content:</label>
+
+ <input type="submit" value="Post">
+ </form>
+
+ <textarea id="content" name="content" cols="80" rows="24" form="postform"></textarea>
+</container>
+
+
+</body>
+</html>
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..ee53f7e
--- /dev/null
+++ b/main.go
@@ -0,0 +1,235 @@
+package main
+
+import (
+ "compress/gzip"
+ "crypto/rand"
+ _ "embed"
+ "encoding/base64"
+ "encoding/gob"
+ "errors"
+ "flag"
+ "html/template"
+ "log"
+ "net/http"
+ "os"
+ "time"
+
+ "github.com/gorilla/mux"
+)
+
+var logger = log.New(os.Stderr, "", 0)
+
+// const ID_BYTES = 5
+const ID_BYTES = 16
+
+//go:embed index.tpl
+var indexTemplateContent string
+var indexTemplate = template.Must(template.New("index").Parse(indexTemplateContent))
+
+//go:embed view.tpl
+var viewTemplateContent string
+var viewTemplate = template.Must(template.New("view").Parse(viewTemplateContent))
+
+//go:embed error.tpl
+var errorTemplateContent string
+var errorTemplate = template.Must(template.New("error").Parse(errorTemplateContent))
+
+//go:embed style.css
+var stylesheetContent []byte
+
+type Paste struct {
+ Id string
+ Title string
+ Content []byte
+}
+
+func (p Paste) GetContent() string {
+ return string(p.Content)
+}
+
+func (p *Paste) Load() error {
+ fh, err := os.Open(p.Id)
+ if err != nil {
+ return err
+ }
+
+ zrdr, err := gzip.NewReader(fh)
+ if err != nil {
+ return err
+ }
+
+ dec := gob.NewDecoder(zrdr)
+ err = dec.Decode(&p)
+ if err != nil {
+ return err
+ }
+
+ zrdr.Close()
+
+ return fh.Close()
+}
+
+func (p Paste) Save() error {
+ fh, err := os.OpenFile(p.Id, os.O_CREATE|os.O_RDWR, 0666)
+ if err != nil {
+ return err
+ }
+
+ zwr := gzip.NewWriter(fh)
+
+ enc := gob.NewEncoder(zwr)
+ err = enc.Encode(&p)
+ if err != nil {
+ return err
+ }
+
+ zwr.Close()
+
+ return fh.Close()
+}
+
+func GenId() string {
+ r := make([]byte, ID_BYTES)
+ _, err := rand.Read(r)
+ if err != nil {
+ logger.Fatal(err)
+ }
+
+ return base64.RawURLEncoding.EncodeToString(r)
+}
+
+func main() {
+ fl := flag.NewFlagSet("Brutally simple pastebin", flag.ExitOnError)
+ listen := fl.String("listen", ":6130", "Address to bind to, LISTEN_ADDR environment variable overrides")
+ debug := fl.Bool("d", false, "debugging add information to the logging output DEBUG=true|false controls this as well")
+ storage := fl.String("s", "", "Directory to serve, must be supplied via flag or STORAGE_DIR environment variable")
+ _ = fl.Parse(os.Args[1:])
+
+ if addr := os.Getenv("LISTEN_ADDR"); addr != "" {
+ *listen = addr
+ }
+ if d := os.Getenv("DEBUG"); d == "true" || *debug {
+ logger.SetFlags(log.LstdFlags | log.Llongfile)
+ }
+ if d := os.Getenv("STORAGE_DIR"); d != "" {
+ *storage = d
+ }
+
+ if *storage == "" {
+ logger.Fatal("Cannot continue without storage directory, set `-s` flag or STORAGE_DIR environment variable")
+ }
+
+ err := os.Chdir(*storage)
+ if err != nil {
+ logger.Fatal(err)
+ }
+
+ mux := mux.NewRouter()
+
+ mux.HandleFunc("/new", newPaste)
+ mux.HandleFunc("/view/{id}", loadPaste)
+ mux.HandleFunc("/style.css", stylesheet)
+ mux.HandleFunc("/", index)
+
+ logger.Println("listening on: ", *listen)
+
+ srv := &http.Server{
+ Handler: mux,
+ Addr: *listen,
+ WriteTimeout: 15 * time.Second,
+ ReadTimeout: 15 * time.Second,
+ }
+ logger.Fatal(srv.ListenAndServe())
+}
+
+func newPaste(w http.ResponseWriter, r *http.Request) {
+ err := r.ParseForm()
+ if err != nil {
+ logger.Println(err)
+ http.Error(w, "Internal server error", http.StatusInternalServerError)
+ return
+ }
+
+ title := r.FormValue("title")
+ content := r.FormValue("content")
+
+ if title == "" || content == "" {
+ logger.Println("Empty title or content")
+ http.Error(w, "Internal server error", http.StatusInternalServerError)
+ return
+ }
+
+ p := &Paste{
+ Id: GenId(),
+ Title: title,
+ Content: []byte(content),
+ }
+
+ err = p.Save()
+ if err != nil {
+ logger.Println(err)
+ http.Error(w, "Internal server error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, "/view/"+p.Id, http.StatusFound)
+}
+
+func loadPaste(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+
+ id, ok := vars["id"]
+ if !ok {
+ w.WriteHeader(http.StatusBadRequest)
+ err := errorTemplate.Execute(w, map[string]string{
+ "Short": "ID Not found",
+ "Long": "There was no ID supplied",
+ })
+ if err != nil {
+ logger.Println(err)
+ }
+ return
+ }
+
+ p := &Paste{Id: id}
+
+ err := p.Load()
+ if err != nil {
+ logger.Println(err)
+ if errors.Is(err, os.ErrNotExist) {
+ w.WriteHeader(http.StatusNotFound)
+ err = errorTemplate.Execute(w, map[string]string{
+ "Short": "ID Not found",
+ "Long": "ID: " + p.Id + "Was not found",
+ })
+ } else {
+ w.WriteHeader(http.StatusInternalServerError)
+ err = errorTemplate.Execute(w, map[string]string{
+ "Short": "ID Not found",
+ "Long": "There was an issue reading ID: " + p.Id,
+ })
+ }
+ if err != nil {
+ logger.Println(err)
+ }
+ return
+ }
+
+ err = viewTemplate.Execute(w, p)
+ if err != nil {
+ logger.Println(err)
+ }
+}
+
+func stylesheet(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Content-type", "text/css")
+ _, _ = w.Write(stylesheetContent)
+}
+
+func index(w http.ResponseWriter, r *http.Request) {
+ err := indexTemplate.Execute(w, nil)
+ if err != nil {
+ logger.Println(err)
+ http.Error(w, "Internal server error", http.StatusInternalServerError)
+ }
+}
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..5b6f128
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,18 @@
+# Brutally simple pastebin
+
+Decided to challenge myself to write a really simple pastebin in go, utilizing
+almost nothing but the standard library in less than an hour.
+
+This is the result of about an hour and a half of work.
+
+It will create a binary with embedded templates. Simply point it at the
+directory in which you'd like to store your pastes.
+
+Each paste is a file, a random id assigned at creation. There's no way to edit
+or change a paste. The only way to delete it is to remove it from the
+filesystem.
+
+There's no indexing function or search either.
+
+There's also absolutely no protection against people spamming the API until
+your disk is full, no authentication either.
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..472b18c
--- /dev/null
+++ b/style.css
@@ -0,0 +1,71 @@
+nav {
+ border-bottom: 3px solid #000;
+ padding-bottom: 10px;
+}
+a {
+ text-decoration: none;
+ color: #268bd2;
+}
+a:visited {
+ color: #d22653;
+}
+a:hover {
+ text-decoration: underline;
+}
+
+code {
+ color: #9672d5;
+ /* background-color: #ff000020; */
+ /* padding: 2px; */
+ font-size: .8em;
+ font-family: "Roboto Mono", "Monaco", "Lucida Console", "DejaVu Sans Mono", "monospace";
+}
+
+pre code {
+ color: #000;
+ background-color: #FFFFEA;
+ display: block;
+ padding: 10px;
+ border: 1px solid;
+ line-height: 1.1;
+ overflow: auto;
+}
+
+
+blockquote {
+ border-left: 4px solid #aaa;
+ padding-left: 1em;
+}
+
+/*
+ * The following was shamelessly ripped from:
+ * http://bettermotherfuckingwebsite.com/
+ * And subsequently modified to suit my needs
+ */
+
+body {
+ margin: 40px auto;
+ max-width: 80%;
+ line-height: 1.6;
+ font-size: 1em;
+ color: #444;
+ padding: 0 10px;
+ /* Added because some browsers don't default to white */
+ background-color: #fff;
+}
+
+img {
+ width: 100%;
+ height: auto;
+}
+
+h1,h2,h3 {
+ line-height: 1.2
+}
+
+@media screen and (min-width: 960px) {
+ body {
+ max-width: 768px;
+ }
+}
+
diff --git a/view.tpl b/view.tpl
new file mode 100644
index 0000000..84c5ebf
--- /dev/null
+++ b/view.tpl
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <link rel="stylesheet" href="/style.css" defer>
+ <title>Paste: {{.Title}}</title>
+</head>
+<body>
+<nav>
+ <a href="/">Home</a>
+ <div style="display: block; float: right;">
+ </div>
+</nav>
+
+<h1>Paste: "{{.Title}}"</h1>
+
+<pre><code>{{.GetContent}}</pre></code>
+
+</body>
+</html>
+