├── .drone.yml ├── .gitignore ├── NEWS ├── READING.adoc ├── README.md ├── UNLICENSE ├── client ├── README.md ├── brpaste.anypaste ├── brpaste.sh └── brpaste.zsh ├── doc └── brpaste.adoc ├── go.mod ├── go.sum ├── http ├── get.go ├── index.go ├── put.go ├── router.go ├── templates.go ├── type.go └── ua_detector.go ├── main.go ├── server ├── README.md ├── brpaste.caddy ├── brpaste.confd └── brpaste.initd ├── storage ├── bolt.go ├── dummy.go ├── dummy_test.go ├── memory.go ├── memory_test.go ├── redis.go ├── redis_test.go └── type.go └── template ├── code.qtpl ├── code.qtpl.go ├── index.qtpl ├── index.qtpl.go ├── layout.qtpl └── layout.qtpl.go /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | name: default 3 | 4 | common: &common 5 | image: golang:alpine 6 | commands: 7 | - CGO_ENABLED=0 go build -v -o "${DRONE_REPO_NAME}-$${GOOS}-$${GOARCH}" -ldflags "-s -w" 8 | 9 | steps: 10 | - name: prepare-templates 11 | image: golang:alpine 12 | commands: 13 | - apk add --no-cache git 14 | - go get github.com/valyala/quicktemplate/qtc 15 | - "$${GOPATH}/bin/qtc" 16 | 17 | - name: build-linux-amd64 18 | <<: *common 19 | environment: 20 | GOOS: linux 21 | GOARCH: amd64 22 | 23 | - name: build-linux-arm64 24 | <<: *common 25 | environment: 26 | GOOS: linux 27 | GOARCH: arm64 28 | 29 | - name: build-linux-ppc64le 30 | <<: *common 31 | environment: 32 | GOOS: linux 33 | GOARCH: ppc64le 34 | 35 | - name: build-dragonfly-amd64 36 | <<: *common 37 | environment: 38 | GOOS: dragonfly 39 | GOARCH: amd64 40 | 41 | - name: build-freebsd-amd64 42 | <<: *common 43 | environment: 44 | GOOS: freebsd 45 | GOARCH: amd64 46 | 47 | - name: build-openbsd-amd64 48 | <<: *common 49 | environment: 50 | GOOS: openbsd 51 | GOARCH: amd64 52 | 53 | #- name: build-windows-amd64 54 | # <<: *common 55 | # environment: 56 | # GOOS: windows 57 | # GOARCH: amd64 58 | 59 | #- name: build-darwin-amd64 60 | # <<: *common 61 | # environment: 62 | # GOOS: darwin 63 | # GOARCH: amd64 64 | 65 | - name: test 66 | image: golang:alpine 67 | commands: 68 | - apk add --no-cache gcc musl-dev # apparently still needed 69 | - go test -v ./... 70 | 71 | - name: deploy 72 | image: plugins/s3 73 | settings: 74 | path_style: true 75 | source: "${DRONE_REPO_NAME}-*" 76 | target: "/${DRONE_REPO_NAME}" 77 | bucket: 78 | from_secret: s3_id 79 | access_key: 80 | from_secret: s3_id 81 | secret_key: 82 | from_secret: s3_key 83 | endpoint: 84 | from_secret: s3_endpoint 85 | when: 86 | branch: 87 | - master 88 | event: 89 | - push 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/brpaste.1 2 | /brpaste 3 | 4 | # must be added manually for releases 5 | /template/*.go 6 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | 2.1.0 (2019-12-19) 2 | 3 | Chloe Kudryavtsev 4 | . init.d script is valid again 5 | . there is now an in-memory storage engine (use in production not recommended) 6 | . consumers of brpaste as a library can now override the default templates 7 | 8 | Haelwenn Monnier 9 | . default template backgrounds default to white 10 | -------------------------------------------------------------------------------- /READING.adoc: -------------------------------------------------------------------------------- 1 | = BRpaste: Reading Guide 2 | Chloe Kudryavtsev 3 | v2.2.0, 2019-01-09 4 | 5 | This document serves as a general guide on how to read the sources of any given release, with the goal of understanding the entirety of the program. 6 | The "version" of this document should coincide with the release you are using. 7 | If there is a newer release than in this document, feel free to open an issue. 8 | 9 | == Overview 10 | `main.go`:: has the code for the binary itself, do not read this first. 11 | `http`:: this directory contains https://github.com/valyala/fasthttp[fasthttp] handlers for the server. 12 | `storage`:: this directory contains various storage methods. 13 | `template`:: this directory contains https://github.com/valyala/quicktemplate[quicktemplate] templates for the server, as well as their compiled forms. `.go` files in that directory should be ignored. 14 | `_test.go`:: all these files are tests, and can be safely ignored, or used as samples of usage. 15 | 16 | == Reading Order 17 | One should start by understanding the `storage` methods and interface. 18 | Once that's done, one can look at the `http` directory. 19 | The `template` directory is mostly superfluous besides knowledge of quicktemplate. 20 | Finally, one can look at `main.go`. 21 | The rest of this section will be detailed explanations of what to look at. 22 | 23 | === Storage 24 | Start by observing `storage/type.go`. 25 | The const variables are sentinel errors, that should be used by compliant storage engines. 26 | The `type storage.Error` stuff makes them legitimate Errors as per go's specs. 27 | Finally, we define the `CHR` interface, which is how all consumers should interact with a given storage engine, past initialization. 28 | 29 | Then, you can look at any storage engine of interest, but one thing to note in `storage/redis.go` is that we use a type alias (`type Redis redis.Client`). 30 | This is done so that we can (effectively) extend it to implement the `CHR` interface. 31 | This means that we can create a `redis.Client` instance, convert it to `storage.Redis` and then use that as a `CHR`. 32 | For an example, see `main.go`. 33 | 34 | === HTTP 35 | First, take a quick look at `http/router.go` which is a handler generator. 36 | Note the routes that are defined using https://github.com/fasthttp/router[fasthttp-router]. 37 | You can see more information on what each route is supposed to do from the user's perspective in the documentation. 38 | All of the routes are implemented using 3 functions total: `Get`, `Put` and `Index`. 39 | 40 | First, let's take a look at the simplest one: `http/index.go`. 41 | It simply renders the configured template. 42 | Templates are set in `storage/templates.go` and can be overridden by API callers. 43 | The defaults, of course, set them to various templates in the `templates` directory, but more on those later. 44 | 45 | Now that we familiarized ourselves with how simple a handler can be, we can look at one of the handler generators. 46 | We'll start with the simpler one: `storage/get.go`. 47 | The first thing to note is that it is passed an instance of `CHR`, which it gets from the generator in `http/router.go`. 48 | It figures out the requested language, and then either returns the raw contents of the paste, or renders the code-viewing template. 49 | 50 | Finally, we can look at `storage/put.go`. 51 | Put has a few extra steps: it calculates a hash of the input content, has more error handling (it can't just 404) and tries to do a smart redirect. 52 | If the UA is determined to be a browser (done by `storage/ua_detector.go`, a simple function), it will do a "See Other" redirection. 53 | Otherwise, it will print the key (hash contents) in plaintext. 54 | 55 | === Templates 56 | The only interesting files inside of the `template` directory are the `.qtpl` ones, as they are not generated (unlike the `.go` files in that directory). 57 | They just contain the HTML that will be used. 58 | 59 | === Main 60 | There is just `main.go` left. 61 | There are a few options: what address to bind to, what storage backend to use, and storage-engine-specific options. 62 | In the `switch s.Storage` block, we initialize the various storage backend options and populate the `store` variable, which will be used generically for the rest of the program. 63 | Then we make sure that the storage was initialized correctly, generate the main handler using `http/router.go` and launch `fasthttp`. 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Burning Rubber Paste 2 | ==================== 3 | 4 | [![Build Status](https://cloud.drone.io/api/badges/5paceToast/brpaste/status.svg)](https://cloud.drone.io/5paceToast/brpaste) 5 | [![Go Report Card](https://goreportcard.com/badge/toast.cafe/x/brpaste)](https://goreportcard.com/report/toast.cafe/x/brpaste) 6 | 7 | `brpaste` is a small and fast pastebin service. 8 | It provides a lightweight REST-like interface and client-side syntax highlighting (if desired). 9 | It's small and fast because it relies on redis to perform the actual storage. 10 | 11 | ### Project Status 12 | Brpaste has been in pure maintenance mode for a while. 13 | Do not mistake the recent (at the time of writing) commits as it being revived: I added boltdb mode to make it easier to migrate (again). 14 | I'm planning to write a new (smaller) thing eventually, but for now I'm still hosting this, and am making it simpler to host. 15 | 16 | ### Quickstart 17 | #### Go edition 18 | `go get -u toast.cafe/x/brpaste` 19 | #### Github edition 20 | Download the correct binary from the releases page. 21 | #### CI (nightly master) edition 22 | Download your build from https://minio.toast.cafe/cicd/brpaste/brpaste-$OS-$ARCH where `$OS` is something like "openbsd" and `$ARCH` is something like "amd64". 23 | Note that the github edition binaries are just these from immediately after a release. 24 | 25 | ### Platform Support 26 | Linux AMD64 is the primary platform. 27 | Everything else is "best effort". 28 | For a full list of supported platforms, see the releases page (all the binaries on there). 29 | 30 | ### Speed 31 | It's just fast. 32 | I could put a bunch of benchmarks here but people didn't really seem to care in the previous version anyway. 33 | Just trust me. 34 | It's fast. 35 | 36 | ### Configuration 37 | There is no configuration within `brpaste`, besides the basics (what address/port to listen on, how to connect to redis). 38 | This is because the actual job being done is relatively minimal. 39 | However, because redis is used, your instance can be greatly configured. 40 | For instance, you could make all your pastes expire in 10 minutes. 41 | Or you could make them never expire. 42 | The hosted instance over at https://brpaste.xyz limits memory usage to 250mb, and expires the least frequently used keys first. 43 | This is the recommended configuration. 44 | 45 | ### Deployment Difficulty 46 | `brpaste` is distributed as a single binary file. 47 | All other files (such as html) are baked into the binary. 48 | 49 | ### Stable IDs 50 | `brpaste` IDs are not the shortest. 51 | What they are, however, is stable. 52 | What does that mean? 53 | When you upload something to `brpaste`, the ID is generated through Murmurhash3 32 bit, and converted into a string of letters and symbols using base64. 54 | Murmurhash3 is suitable for lookups, so collision are sufficiently unlikely within the lifetime of your paste. 55 | However, if you upload the same paste twice, the ID will stay the same. 56 | The memory usage will not increase. 57 | This unlocks a few interesting use-cases (e.g not needing to keep around an open tab of a paste - just paste it again, you'll get the original link back). 58 | The bitsize of the ID is 32 bits, which translates to roughly 6 base64 "digits". 59 | This may not be the shortest, but it is short enough to memorize in one go (see: magic 7 of human working memory; approximated to more likely 6 if letters are involved). 60 | As such, the disadvantage is rather minimal, while the advantage is a nice-to-have, consistent, and cheap (murmurhash3 is fast, and there's no need to do things like keep a counter around). 61 | 62 | ### Other Utilities 63 | For server-side helpers and utilities (such as openrc scripts, systemd unit files, and anything else of the sort), see the `server/` directory. 64 | For client-side helpers and utilities (such as a `sprunge(1)`-like script, an `anypaste` plugin, and anything else of the sort), see the `client/` directory. 65 | For documentation, see the `doc/` directory, but do note that you will need a compliant asciidoc parser to compile it. 66 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | Burning Rubber Paste 2 | ==================== 3 | 4 | Client Side Utilities 5 | --------------------- 6 | 7 | ### `brpaste.anypaste` 8 | An [anypaste](https://anypaste.xyz/) plugin for `brpaste`. 9 | 10 | ### `brpaste.sh` 11 | A shell script to paste to `brpaste`. 12 | 13 | ### `brpaste.zsh` 14 | A zsh-style script/function-definition combo for pasting to `brpaste`. 15 | -------------------------------------------------------------------------------- /client/brpaste.anypaste: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | : "${brpaste_host:=https://brpaste.xyz}" 4 | 5 | case $1 in 6 | check_eligibility) 7 | [[ $ap_mime == text/* ]] 8 | ;; 9 | upload) 10 | brpaste_id=$(curl -#fF "data=<$ap_path" "$brpaste_host") \ 11 | || { echo 'ERROR: Upload failed!' >&2 && exit 1; } 12 | echo 13 | echo "Link: $brpaste_host/$brpaste_id" 14 | echo "Direct: $brpaste_host/raw/$brpaste_id" 15 | echo 16 | ;; 17 | get_info) 18 | echo '[name]' 19 | echo 'BRPaste' 20 | echo '[description]' 21 | echo 'A pastebin so fast, it burns rubber.' 22 | echo '[tags]' 23 | echo 'private' 24 | echo 'direct' 25 | echo 26 | echo '[optional_config]' 27 | echo 'brpaste_host:Hosted instance to use (default: https://brpaste.xyz)' 28 | esac 29 | -------------------------------------------------------------------------------- /client/brpaste.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | brpaste() { 3 | host='https://brpaste.xyz' 4 | [ $# -eq 0 ] && set -- '-' 5 | brpaste_id=$(curl -#fF "data=<$1" "$host") \ 6 | || { echo 'ERROR: Upload failed!' >&2 && exit 1; } 7 | printf '%s/%s\n' "$host" "$brpaste_id" 8 | } 9 | 10 | return 0 2>/dev/null || brpaste "$@" 11 | -------------------------------------------------------------------------------- /client/brpaste.zsh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | host='https://brpaste.xyz' 3 | [[ $# -eq 0 ]] && set -- '-' 4 | brpaste_id=$(curl -\#fF "data=<$1" "$host") \ 5 | || { echo 'ERROR: Upload failed!' >&2 && return 1; } 6 | printf '%s/%s\n' "$host" "$brpaste_id" 7 | -------------------------------------------------------------------------------- /doc/brpaste.adoc: -------------------------------------------------------------------------------- 1 | = brpaste(1) 2 | Chloe Kudryavtsev 3 | v2.0.0 4 | :doctype: manpage 5 | 6 | == Name 7 | 8 | brpaste - a pastebin service that's so fast, it burns rubber. 9 | 10 | == Synopsis 11 | 12 | *brpaste* [_OPTIONS_] 13 | 14 | == Options 15 | 16 | *-bind*=_BIND_ADDRESS_:: 17 | Bind to address _BIND_ADDRESS_. 18 | *-redis*=_REDIS_URI_:: 19 | Connect to Redis using _REDIS_URI_. 20 | *-storage*=_STORAGE_SYSTEM_:: 21 | What storage system to use. See valid options in <<_storage_systems>>. 22 | 23 | == Endpoints 24 | 25 | _GET_ */*:: 26 | Index. 27 | Provides simplified table of this section. 28 | Provides examples. 29 | Includes submission form. 30 | 31 | _GET_ */:id*:: 32 | User-facing viewer of paste _id_. 33 | Standard view. 34 | 35 | _GET_ */:id/:syntax*:: 36 | Same as */:id*. 37 | If javascript is available, attempt to highlight the paste as _syntax_. 38 | 39 | _GET_ */:id/raw*:: 40 | Raw contents of paste _id_. 41 | Mime is set to plaintext. 42 | 43 | _POST_ */* _data_=*content*:: 44 | Pastebin *content*. 45 | The _id_ will be the the base64 representation of content's murmurhash3 hash. 46 | If that _id_ already exists, it is overwritten. 47 | 48 | _PUT_ */:id* _data_=*content*:: 49 | Put *content* into _id_. 50 | If _id_ already exists, abort. 51 | 52 | == Storage Systems 53 | 54 | Memory:: 55 | Selected with "memory". 56 | Not persistent, but very fast. 57 | 58 | Redis:: 59 | Selected with "redis". 60 | Also the default and recommended option. 61 | Fast and simple. 62 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module toast.cafe/x/brpaste/v2 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/fasthttp/router v0.5.2 7 | github.com/go-redis/redis/v7 v7.0.0-beta.4 8 | github.com/golang/protobuf v1.3.1 // indirect 9 | github.com/klauspost/compress v1.9.4 // indirect 10 | github.com/stretchr/testify v1.8.4 11 | github.com/twmb/murmur3 v1.0.0 12 | github.com/valyala/fasthttp v1.7.0 13 | github.com/valyala/quicktemplate v1.4.1 14 | go.etcd.io/bbolt v1.3.8 15 | golang.org/x/sys v0.15.0 // indirect 16 | golang.org/x/text v0.3.2 // indirect 17 | gopkg.in/yaml.v2 v2.4.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fasthttp/router v0.5.2 h1:xdmx8uYc9IFDtlbG2/FhE1Gyowv7/sqMgMonRjoW0Yo= 5 | github.com/fasthttp/router v0.5.2/go.mod h1:Y5JAeRTSPwSLoUgH4x75UnT1j1IcAgVshMDMMrnNmKQ= 6 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 7 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 8 | github.com/go-redis/redis/v7 v7.0.0-beta.4 h1:p6z7Pde69EGRWvlC++y8aFcaWegyrKHzOBGo0zUACTQ= 9 | github.com/go-redis/redis/v7 v7.0.0-beta.4/go.mod h1:xhhSbUMTsleRPur+Vgx9sUHtyN33bdjxY+9/0n9Ig8s= 10 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 11 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 12 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 13 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 14 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 15 | github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 16 | github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 17 | github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 18 | github.com/klauspost/compress v1.9.4 h1:xhvAeUPQ2drNUhKtrGdTGNvV9nNafHMUkRyLkzxJoB4= 19 | github.com/klauspost/compress v1.9.4/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 20 | github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 21 | github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 22 | github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 23 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 24 | github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= 25 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 26 | github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= 27 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 28 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 29 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 30 | github.com/savsgio/gotils v0.0.0-20190925070755-524bc4f47500 h1:9Pi10H7E8E79/x2HSe1FmMGd7BJ1WAqDKzwjpv+ojFg= 31 | github.com/savsgio/gotils v0.0.0-20190925070755-524bc4f47500/go.mod h1:lHhJedqxCoHN+zMtwGNTXWmF0u9Jt363FYRhV6g0CdY= 32 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 33 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 34 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 35 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 36 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 37 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 38 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 39 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 40 | github.com/twmb/murmur3 v1.0.0 h1:MLMwMEQRKsu94uJnoveYjjHmcLwI3HNcWXP4LJuNe3I= 41 | github.com/twmb/murmur3 v1.0.0/go.mod h1:5Y5m8Y8WIyucaICVP+Aep5C8ydggjEuRQHDq1icoOYo= 42 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 43 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 44 | github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= 45 | github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= 46 | github.com/valyala/fasthttp v1.7.0 h1:r0Z/fzm2Euss3K0hhkPtt41TigAr9bXueavuXgrThi4= 47 | github.com/valyala/fasthttp v1.7.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= 48 | github.com/valyala/quicktemplate v1.4.1 h1:tEtkSN6mTCJlYVT7As5x4wjtkk2hj2thsb0M+AcAVeM= 49 | github.com/valyala/quicktemplate v1.4.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4= 50 | github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= 51 | go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= 52 | go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= 53 | go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M= 54 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 55 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 56 | golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 57 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= 58 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 59 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 60 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 61 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 62 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 64 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 65 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 66 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 67 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 68 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 69 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 70 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 71 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 72 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 73 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 74 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 75 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 76 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 77 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 78 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 79 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 80 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 81 | -------------------------------------------------------------------------------- /http/get.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/valyala/fasthttp" 5 | "toast.cafe/x/brpaste/v2/storage" 6 | ) 7 | 8 | // Get generates a handler for the /:key[/:lang] endpoint 9 | func Get(store storage.CHR) handler { 10 | return func(ctx *fasthttp.RequestCtx) { 11 | ukey := ctx.UserValue("key") 12 | ulang := ctx.UserValue("lang") 13 | 14 | var key, lang string 15 | key = ukey.(string) // there's no recovering otherwise 16 | if ulang != nil { 17 | lang = ulang.(string) 18 | } 19 | 20 | res, err := store.Read(key) 21 | switch err { 22 | case storage.Unhealthy: 23 | ctx.Error("Backend did not respond", fasthttp.StatusInternalServerError) 24 | case nil: // all good 25 | if lang == "raw" { 26 | ctx.SuccessString("text/plain", res) 27 | } else { 28 | ctx.SuccessString("text/html", CodeTemplate(lang, res)) // render template 29 | } 30 | default: 31 | ctx.NotFound() 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /http/index.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/valyala/fasthttp" 5 | ) 6 | 7 | // Index handles the / endpoint 8 | func Index(ctx *fasthttp.RequestCtx) { 9 | ctx.SuccessString("text/html", IndexTemplate()) // render template 10 | } 11 | -------------------------------------------------------------------------------- /http/put.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | 7 | "github.com/twmb/murmur3" 8 | "github.com/valyala/fasthttp" 9 | "toast.cafe/x/brpaste/v2/storage" 10 | ) 11 | 12 | // Put generates a handler for the POST / and PUT /:key endpoints 13 | func Put(store storage.CHR, put bool) handler { 14 | return func(ctx *fasthttp.RequestCtx) { 15 | data := ctx.FormValue("data") 16 | if len(data) == 0 { // works with nil 17 | ctx.Error("Missing data field", fasthttp.StatusBadRequest) 18 | return 19 | } 20 | 21 | ukey := ctx.UserValue("key") 22 | var key string 23 | if ukey != nil { 24 | key = ukey.(string) 25 | } else { 26 | hasher := murmur3.New32() 27 | hasher.Write(data) 28 | keybuf := hasher.Sum(nil) 29 | key = base64.RawURLEncoding.EncodeToString(keybuf) 30 | } 31 | val := string(data) 32 | 33 | err := store.Create(key, val, put) 34 | 35 | switch err { 36 | case storage.Collision: 37 | ctx.Error("Collision detected when undesired", fasthttp.StatusConflict) 38 | case storage.Unhealthy: 39 | ctx.Error("Backend did not respond", fasthttp.StatusInternalServerError) 40 | case nil: // everything succeeded 41 | if isBrowser(string(ctx.UserAgent())) { 42 | ctx.Redirect(fmt.Sprintf("/%s", key), fasthttp.StatusSeeOther) 43 | } else { 44 | ctx.SetStatusCode(fasthttp.StatusCreated) 45 | ctx.SetBodyString(key) 46 | } 47 | default: 48 | ctx.Error(err.Error(), fasthttp.StatusInternalServerError) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /http/router.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/fasthttp/router" 5 | "github.com/valyala/fasthttp" 6 | "toast.cafe/x/brpaste/v2/storage" 7 | ) 8 | 9 | // GenHandler generates the brpaste handler 10 | func GenHandler(store storage.CHR) func(ctx *fasthttp.RequestCtx) { 11 | get := Get(store) 12 | post := Put(store, false) 13 | put := Put(store, true) 14 | 15 | r := router.New() 16 | r.GET("/", Index) 17 | r.GET("/:key", get) 18 | r.GET("/:key/:lang", get) 19 | r.POST("/", post) 20 | r.PUT("/:key", put) 21 | 22 | return r.Handler 23 | } 24 | -------------------------------------------------------------------------------- /http/templates.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import "toast.cafe/x/brpaste/v2/template" 4 | 5 | var ( 6 | // CodeTemplate is the function to be used as the template for the code viewer 7 | // overwrite it before calling GenHandler 8 | CodeTemplate = template.Code 9 | // IndexTemplate is the function to be used as the template for the index page 10 | // overwrite it before calling GenHandler 11 | IndexTemplate = template.Index 12 | ) 13 | -------------------------------------------------------------------------------- /http/type.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import "github.com/valyala/fasthttp" 4 | 5 | type handler = fasthttp.RequestHandler 6 | -------------------------------------------------------------------------------- /http/ua_detector.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import "strings" 4 | 5 | var ( 6 | browsers = []string{ 7 | "Firefox/", 8 | "Chrome/", 9 | "Safari/", 10 | "OPR/", 11 | "Edge/", 12 | "Trident/", 13 | } 14 | ) 15 | 16 | func isBrowser(ua string) bool { 17 | for _, el := range browsers { 18 | if strings.Contains(ua, el) { 19 | return true 20 | } 21 | } 22 | return false 23 | } 24 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "flag" 6 | "os" 7 | 8 | bolt "go.etcd.io/bbolt" 9 | "github.com/go-redis/redis/v7" 10 | "github.com/valyala/fasthttp" 11 | "toast.cafe/x/brpaste/v2/http" 12 | "toast.cafe/x/brpaste/v2/storage" 13 | ) 14 | 15 | var s settings 16 | 17 | type settings struct { 18 | Bind string 19 | Bolt string 20 | Redis string 21 | Storage string 22 | } 23 | 24 | func main() { 25 | // ---- Flags 26 | flag.StringVar(&s.Bind, "bind", ":8080", "address to bind to") 27 | flag.StringVar(&s.Bolt, "bolt", "brpaste.db", "bolt database file to use") 28 | flag.StringVar(&s.Redis, "redis", "redis://localhost:6379", "redis connection string") 29 | flag.StringVar(&s.Storage, "storage", "bolt", "type of storage to use") 30 | flag.Parse() 31 | 32 | // ---- Storage system 33 | var store storage.CHR 34 | 35 | switch s.Storage { 36 | case "memory": 37 | store = storage.NewMemory() 38 | case "redis": 39 | redisOpts, err := redis.ParseURL(s.Redis) 40 | if err != nil { 41 | fmt.Fprintf(os.Stderr, "Could not parse redis connection string %s\n", s.Redis) 42 | os.Exit(1) 43 | } 44 | client := redis.NewClient(redisOpts) 45 | store = (*storage.Redis)(client) 46 | case "bolt": 47 | db, err := bolt.Open(s.Bolt, 0600, nil) 48 | if err != nil { 49 | fmt.Fprintf(os.Stderr, "Failed to open/create boltdb database at %s\n", s.Bolt) 50 | os.Exit(1) 51 | } 52 | store, err = storage.OpenBolt(db) 53 | if err != nil { 54 | fmt.Fprintf(os.Stderr, "Failed to initialize boltdb database at %s: %s\n", s.Bolt, err) 55 | os.Exit(1) 56 | } 57 | defer db.Close() 58 | default: 59 | fmt.Fprintf(os.Stderr, "Could not figure out which storage system to use, tried %s\n", s.Storage) 60 | os.Exit(1) 61 | } 62 | 63 | // ---- Is storage healthy? 64 | if !store.Healthy() { 65 | fmt.Fprintf(os.Stderr, "Storage is unhealthy, cannot proceed.\n") 66 | os.Exit(1) 67 | } 68 | 69 | // ---- Start! 70 | handler := http.GenHandler(store) 71 | fasthttp.ListenAndServe(s.Bind, handler) 72 | } 73 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | Burning Rubber Paste 2 | ==================== 3 | 4 | Server Side Utilities 5 | --------------------- 6 | 7 | ### `brpaste.caddy` 8 | A [caddy](https://caddyserver.com/) example for `brpaste`. 9 | 10 | ### `brpaste.confd` and `brpaste.initd` 11 | An [openrc](https://wiki.gentoo.org/wiki/Project:OpenRC) service definition for `brpaste`. 12 | -------------------------------------------------------------------------------- /server/brpaste.caddy: -------------------------------------------------------------------------------- 1 | paste.example.com { 2 | proxy / localhost:8080 3 | } 4 | -------------------------------------------------------------------------------- /server/brpaste.confd: -------------------------------------------------------------------------------- 1 | # defaults to localhost, set to :: to listen on all 2 | #BRPASTE_ADDR=:8080 3 | 4 | # if your redis is listening on a different address 5 | # accepts redis:// style strings 6 | #BRPASTE_REDIS= 7 | -------------------------------------------------------------------------------- /server/brpaste.initd: -------------------------------------------------------------------------------- 1 | #!/sbin/openrc-run 2 | supervisor=supervise-daemon 3 | 4 | respawn_delay=30 5 | 6 | command_user="nobody:nobody" 7 | command=/usr/bin/brpaste 8 | command_args="\ 9 | ${BRPASTE_REDIS:+-redis $BRPASTE_REDIS} \ 10 | ${BRPASTE_ADDR:+-bind $BRPASTE_ADDR}" 11 | 12 | depend() { 13 | need redis 14 | } 15 | -------------------------------------------------------------------------------- /storage/bolt.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import bolt "go.etcd.io/bbolt" 4 | 5 | var _ CHR = &Bolt{} 6 | 7 | // Redis storage engine 8 | type Bolt bolt.DB 9 | 10 | var bname = []byte("brpaste") 11 | 12 | func OpenBolt(db *bolt.DB) (*Bolt, error) { 13 | err := db.Update(func(tx *bolt.Tx) error { 14 | _, err := tx.CreateBucketIfNotExists(bname) 15 | return err 16 | }) 17 | return (*Bolt)(db), err 18 | } 19 | 20 | // Create an entry in redis 21 | func (db *Bolt) Create(key, value string, checkcollision bool) error { 22 | err := (*bolt.DB)(db).Update(func(tx *bolt.Tx) error { 23 | b := tx.Bucket(bname) 24 | if b == nil { 25 | return Unhealthy 26 | } 27 | k := []byte(key) 28 | if checkcollision { 29 | if b.Get(k) != nil { 30 | return Collision 31 | } 32 | } 33 | return b.Put(k, []byte(value)) 34 | }) 35 | if err != nil { 36 | return err 37 | } 38 | return nil 39 | } 40 | 41 | func (db *Bolt) Read(key string) (string, error) { 42 | var out string 43 | err := (*bolt.DB)(db).View(func(tx *bolt.Tx) error { 44 | b := tx.Bucket(bname) 45 | if b == nil { 46 | return Unhealthy 47 | } 48 | v := b.Get([]byte(key)) 49 | if v == nil { 50 | return NotFound 51 | } 52 | out = string(v) 53 | return nil 54 | }) 55 | if err != nil { 56 | return "", err 57 | } 58 | return out, nil 59 | } 60 | 61 | // Healthy TODO: destub ? 62 | func (db *Bolt) Healthy() bool { 63 | return db != nil 64 | } 65 | -------------------------------------------------------------------------------- /storage/dummy.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | var _ CHR = &Dummy{} 4 | 5 | // Dummy is a dummy storage device that acts predictably for tests 6 | type Dummy struct { 7 | collide, found, healthy bool 8 | err error 9 | } 10 | 11 | // Create is a noop 12 | // if the backend was created unhealthy, it will error out with the unhealthy error 13 | // if the backend was set as colliding, and collision detection is enabled, it will error out with a collision 14 | // if the backend was set as erroring, it will error with that error 15 | func (d *Dummy) Create(key, value string, checkcollision bool) error { 16 | if !d.Healthy() { 17 | return Unhealthy 18 | } 19 | if checkcollision && d.collide { 20 | return Collision 21 | } 22 | if d.err != nil { 23 | return d.err 24 | } 25 | return nil 26 | } 27 | 28 | // Read will return the key that is sent to it, or error out if the backend was created unhealthy 29 | // Read echoes the key given to it back 30 | // if the backend was set to unhealthy, it also returns the Unhealthy error 31 | func (d *Dummy) Read(key string) (string, error) { 32 | var err error 33 | if !d.Healthy() { 34 | err = Unhealthy 35 | } 36 | if !d.found { 37 | err = NotFound 38 | } 39 | return key, err 40 | } 41 | 42 | // Healthy checks whether or not the backend was created healthy 43 | func (d *Dummy) Healthy() bool { return d.healthy } 44 | -------------------------------------------------------------------------------- /storage/dummy_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestDummy(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | const ( 13 | k = "key" 14 | v = "val" 15 | ) 16 | 17 | var ( 18 | // setup 19 | err = Error("testing error") 20 | dumC = Dummy{collide: true, found: true, healthy: true, err: nil} 21 | dumE = Dummy{collide: false, found: true, healthy: true, err: err} 22 | dumF = Dummy{collide: false, found: false, healthy: true, err: nil} 23 | dumH = Dummy{collide: false, found: true, healthy: false, err: nil} 24 | dumN = Dummy{collide: false, found: true, healthy: true, err: nil} 25 | 26 | // ---- TESTS 27 | // Health 28 | hc = dumC.Healthy() 29 | he = dumE.Healthy() 30 | hf = dumF.Healthy() 31 | hh = dumH.Healthy() 32 | hn = dumN.Healthy() 33 | 34 | // Write - no collisions 35 | wc = dumC.Create(k, v, false) 36 | we = dumE.Create(k, v, false) 37 | wf = dumF.Create(k, v, false) 38 | wh = dumH.Create(k, v, false) 39 | wn = dumN.Create(k, v, false) 40 | 41 | // Write - collisions 42 | cc = dumC.Create(k, v, true) 43 | ce = dumE.Create(k, v, true) 44 | cf = dumF.Create(k, v, true) 45 | ch = dumH.Create(k, v, true) 46 | cn = dumN.Create(k, v, true) 47 | 48 | // Read 49 | rc, erc = dumC.Read(k) 50 | re, ere = dumE.Read(k) 51 | rf, erf = dumF.Read(k) 52 | rh, erh = dumH.Read(k) 53 | rn, ern = dumN.Read(k) 54 | ) 55 | assert.Equal("testing error", err.Error()) 56 | 57 | assert.True(hc) 58 | assert.True(he) 59 | assert.True(hf) 60 | assert.False(hh) 61 | assert.True(hn) 62 | 63 | assert.Nil(wc) 64 | assert.NotNil(we) 65 | assert.Nil(wf) 66 | assert.NotNil(wh) 67 | assert.Nil(wn) 68 | 69 | assert.Equal(err, we) 70 | assert.Equal(Unhealthy, wh) 71 | 72 | assert.NotNil(cc) 73 | assert.NotNil(ce) 74 | assert.Nil(cf) 75 | assert.NotNil(ch) 76 | assert.Nil(cn) 77 | 78 | assert.Equal(Collision, cc) 79 | assert.Equal(err, ce) 80 | assert.Equal(Unhealthy, ch) 81 | 82 | assert.Nil(erc) 83 | assert.Nil(ere) 84 | assert.NotNil(erf) 85 | assert.NotNil(erh) 86 | assert.Nil(ern) 87 | 88 | assert.Equal(Unhealthy, erh) 89 | assert.Equal(NotFound, erf) 90 | 91 | assert.Equal(k, rc) 92 | assert.Equal(k, re) 93 | assert.Equal(k, rf) 94 | assert.Equal(k, rh) 95 | assert.Equal(k, rn) 96 | } 97 | -------------------------------------------------------------------------------- /storage/memory.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | var _ CHR = &Memory{} 4 | 5 | // Memory is an in-memory non-persistent storage type 6 | // using it in production is not recommended 7 | type Memory struct { 8 | store map[string]string 9 | } 10 | 11 | // Create will store a value in a key in memory 12 | func (r *Memory) Create(key, value string, checkcollision bool) error { 13 | if !r.Healthy() { 14 | return Unhealthy 15 | } 16 | if checkcollision { 17 | if _, ok := r.store[key]; ok { 18 | return Collision 19 | } 20 | } 21 | r.store[key] = value 22 | return nil 23 | } 24 | 25 | // Read will read a key from memory, if it's there 26 | func (r *Memory) Read(key string) (string, error) { 27 | if !r.Healthy() { 28 | return "", Unhealthy 29 | } 30 | if val, ok := r.store[key]; ok { 31 | return val, nil 32 | } 33 | return "", Error("value not found") 34 | } 35 | 36 | // Healthy checks if the memory storage is initialized 37 | func (r *Memory) Healthy() bool { 38 | return r.store != nil 39 | } 40 | 41 | // NewMemory initializes a memory backend for use 42 | func NewMemory() CHR { 43 | m := Memory{ 44 | store: make(map[string]string), 45 | } 46 | return &m 47 | } 48 | -------------------------------------------------------------------------------- /storage/memory_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestMemory(t *testing.T) { 10 | assert := assert.New(t) 11 | const ( 12 | k = "key" 13 | v1 = "val1" 14 | v2 = "val2" 15 | ) 16 | 17 | m := NewMemory() 18 | 19 | err := m.Create(k, v1, false) 20 | assert.Nil(err) 21 | 22 | err = m.Create(k, v2, false) 23 | assert.Nil(err) 24 | 25 | err = m.Create(k, v1, true) 26 | assert.Equal(Collision, err) 27 | 28 | v, err := m.Read(k) 29 | assert.Nil(err) 30 | assert.Equal(v2, v) 31 | 32 | _, err = m.Read(v1) 33 | assert.NotNil(err) 34 | assert.Equal(NotFound, err) 35 | } 36 | 37 | func TestUnhealthyMemory(t *testing.T) { 38 | assert := assert.New(t) 39 | m := &Memory{} 40 | 41 | var ( 42 | e1 = m.Create("", "", false) 43 | e2 = m.Create("", "", true) 44 | _, e3 = m.Read("") 45 | ) 46 | 47 | assert.NotNil(e1) 48 | assert.NotNil(e2) 49 | assert.NotNil(e3) 50 | 51 | assert.Equal(Unhealthy, e1) 52 | assert.Equal(Unhealthy, e2) 53 | assert.Equal(Unhealthy, e3) 54 | 55 | assert.False(m.Healthy()) 56 | } 57 | -------------------------------------------------------------------------------- /storage/redis.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import "github.com/go-redis/redis/v7" 4 | 5 | var _ CHR = &Redis{} 6 | 7 | // Redis storage engine 8 | type Redis redis.Client 9 | 10 | // Create an entry in redis 11 | func (r *Redis) Create(key, value string, checkcollision bool) error { 12 | if !r.Healthy() { 13 | return Unhealthy 14 | } 15 | if checkcollision { 16 | col, err := r.Exists(key).Result() 17 | if err != nil { 18 | return Unhealthy 19 | } 20 | if col > 0 { 21 | return Collision 22 | } 23 | } 24 | _, err := r.Set(key, value, 0).Result() 25 | return err 26 | } 27 | 28 | func (r *Redis) Read(key string) (string, error) { 29 | if !r.Healthy() { 30 | return "", Unhealthy 31 | } 32 | res, err := r.Get(key).Result() 33 | if err == redis.Nil { 34 | return res, NotFound 35 | } 36 | return res, err 37 | } 38 | 39 | // Healthy determines whether redis is responding to pings 40 | func (r *Redis) Healthy() bool { 41 | _, err := r.Ping().Result() 42 | if err != nil { 43 | return false 44 | } 45 | return true 46 | } 47 | -------------------------------------------------------------------------------- /storage/redis_test.go: -------------------------------------------------------------------------------- 1 | // +build redis 2 | 3 | package storage 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/go-redis/redis/v7" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestRedis(t *testing.T) { 13 | assert := assert.New(t) 14 | const ( 15 | k = "key" 16 | v1 = "val1" 17 | v2 = "val2" 18 | ) 19 | 20 | // when using tag "redis", we expect all of this to be as-is 21 | var ( 22 | o, _ = redis.ParseURL("redis://localhost:6379") 23 | c = redis.NewClient(o) 24 | s = (*Redis)(c) 25 | ) 26 | 27 | assert.True(s.Healthy()) 28 | 29 | err := s.Create(k, v1, false) 30 | assert.Nil(err) 31 | 32 | err = s.Create(k, v2, false) 33 | assert.Nil(err) 34 | 35 | err = s.Create(k, v1, true) 36 | assert.Equal(Collision, err) 37 | 38 | v, err := s.Read(k) 39 | assert.Nil(err) 40 | assert.Equal(v2, v) 41 | 42 | _, err = s.Read(v1) 43 | assert.NotNil(err) 44 | assert.Equal(NotFound, err) 45 | } 46 | -------------------------------------------------------------------------------- /storage/type.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | const ( 4 | // Collision is a fatal collision error, only triggered when it fails 5 | Collision = Error("collision detected") 6 | // NotFound is a fatal error when the backend cannot find a key to read 7 | NotFound = Error("value not found") 8 | // Unhealthy is a fatal error when the backend ceases to be healthy 9 | Unhealthy = Error("backend unhealthy") 10 | ) 11 | 12 | // Error is a sentinel error type for storage engines 13 | type Error string 14 | 15 | func (e Error) Error() string { return string(e) } 16 | 17 | // CHR - Create, Health, Read 18 | type CHR interface { 19 | Create(key, value string, checkcollision bool) error 20 | Healthy() bool 21 | Read(key string) (string, error) 22 | } 23 | -------------------------------------------------------------------------------- /template/code.qtpl: -------------------------------------------------------------------------------- 1 | The code layout. 2 | {% func Code(lang, data string) %} 3 | {%= layout(code_css(), code_scripts(lang), "", code_contents(lang, data), code_bodyscripts(lang)) %} 4 | {% endfunc %} 5 | 6 | {% code 7 | const prefix = "https://unpkg.com/prismjs" 8 | %} 9 | 10 | The code scripts. 11 | {% func code_scripts(lang string) %} 12 | 13 | {% endfunc %} 14 | 15 | The code bodyscripts. 16 | {% func code_bodyscripts(lang string) %} 17 | {% stripspace %} 18 | 19 | {% if lang != "" && lang != "none" %} 20 | 21 | {% endif %} 22 | {% endstripspace %} 23 | {% endfunc %} 24 | 25 | The code css. 26 | {% func code_css() %} 27 | 32 | {% endfunc %} 33 | 34 | The code contents. 35 | {% func code_contents(lang, data string) %} 36 |
{%s data %}
37 | {% endfunc %} 38 | -------------------------------------------------------------------------------- /template/code.qtpl.go: -------------------------------------------------------------------------------- 1 | // Code generated by qtc from "code.qtpl". DO NOT EDIT. 2 | // See https://github.com/valyala/quicktemplate for details. 3 | 4 | // The code layout. 5 | 6 | //line template/code.qtpl:2 7 | package template 8 | 9 | //line template/code.qtpl:2 10 | import ( 11 | qtio422016 "io" 12 | 13 | qt422016 "github.com/valyala/quicktemplate" 14 | ) 15 | 16 | //line template/code.qtpl:2 17 | var ( 18 | _ = qtio422016.Copy 19 | _ = qt422016.AcquireByteBuffer 20 | ) 21 | 22 | //line template/code.qtpl:2 23 | func StreamCode(qw422016 *qt422016.Writer, lang, data string) { 24 | //line template/code.qtpl:2 25 | qw422016.N().S(` 26 | `) 27 | //line template/code.qtpl:3 28 | streamlayout(qw422016, code_css(), code_scripts(lang), "", code_contents(lang, data), code_bodyscripts(lang)) 29 | //line template/code.qtpl:3 30 | qw422016.N().S(` 31 | `) 32 | //line template/code.qtpl:4 33 | } 34 | 35 | //line template/code.qtpl:4 36 | func WriteCode(qq422016 qtio422016.Writer, lang, data string) { 37 | //line template/code.qtpl:4 38 | qw422016 := qt422016.AcquireWriter(qq422016) 39 | //line template/code.qtpl:4 40 | StreamCode(qw422016, lang, data) 41 | //line template/code.qtpl:4 42 | qt422016.ReleaseWriter(qw422016) 43 | //line template/code.qtpl:4 44 | } 45 | 46 | //line template/code.qtpl:4 47 | func Code(lang, data string) string { 48 | //line template/code.qtpl:4 49 | qb422016 := qt422016.AcquireByteBuffer() 50 | //line template/code.qtpl:4 51 | WriteCode(qb422016, lang, data) 52 | //line template/code.qtpl:4 53 | qs422016 := string(qb422016.B) 54 | //line template/code.qtpl:4 55 | qt422016.ReleaseByteBuffer(qb422016) 56 | //line template/code.qtpl:4 57 | return qs422016 58 | //line template/code.qtpl:4 59 | } 60 | 61 | //line template/code.qtpl:7 62 | const prefix = "https://unpkg.com/prismjs" 63 | 64 | // The code scripts. 65 | 66 | //line template/code.qtpl:11 67 | func streamcode_scripts(qw422016 *qt422016.Writer, lang string) { 68 | //line template/code.qtpl:11 69 | qw422016.N().S(` 70 | 75 | `) 76 | //line template/code.qtpl:13 77 | } 78 | 79 | //line template/code.qtpl:13 80 | func writecode_scripts(qq422016 qtio422016.Writer, lang string) { 81 | //line template/code.qtpl:13 82 | qw422016 := qt422016.AcquireWriter(qq422016) 83 | //line template/code.qtpl:13 84 | streamcode_scripts(qw422016, lang) 85 | //line template/code.qtpl:13 86 | qt422016.ReleaseWriter(qw422016) 87 | //line template/code.qtpl:13 88 | } 89 | 90 | //line template/code.qtpl:13 91 | func code_scripts(lang string) string { 92 | //line template/code.qtpl:13 93 | qb422016 := qt422016.AcquireByteBuffer() 94 | //line template/code.qtpl:13 95 | writecode_scripts(qb422016, lang) 96 | //line template/code.qtpl:13 97 | qs422016 := string(qb422016.B) 98 | //line template/code.qtpl:13 99 | qt422016.ReleaseByteBuffer(qb422016) 100 | //line template/code.qtpl:13 101 | return qs422016 102 | //line template/code.qtpl:13 103 | } 104 | 105 | // The code bodyscripts. 106 | 107 | //line template/code.qtpl:16 108 | func streamcode_bodyscripts(qw422016 *qt422016.Writer, lang string) { 109 | //line template/code.qtpl:16 110 | qw422016.N().S(` 111 | `) 112 | //line template/code.qtpl:17 113 | qw422016.N().S(``) 118 | //line template/code.qtpl:19 119 | if lang != "" && lang != "none" { 120 | //line template/code.qtpl:19 121 | qw422016.N().S(``) 130 | //line template/code.qtpl:21 131 | } 132 | //line template/code.qtpl:22 133 | qw422016.N().S(` 134 | `) 135 | //line template/code.qtpl:23 136 | } 137 | 138 | //line template/code.qtpl:23 139 | func writecode_bodyscripts(qq422016 qtio422016.Writer, lang string) { 140 | //line template/code.qtpl:23 141 | qw422016 := qt422016.AcquireWriter(qq422016) 142 | //line template/code.qtpl:23 143 | streamcode_bodyscripts(qw422016, lang) 144 | //line template/code.qtpl:23 145 | qt422016.ReleaseWriter(qw422016) 146 | //line template/code.qtpl:23 147 | } 148 | 149 | //line template/code.qtpl:23 150 | func code_bodyscripts(lang string) string { 151 | //line template/code.qtpl:23 152 | qb422016 := qt422016.AcquireByteBuffer() 153 | //line template/code.qtpl:23 154 | writecode_bodyscripts(qb422016, lang) 155 | //line template/code.qtpl:23 156 | qs422016 := string(qb422016.B) 157 | //line template/code.qtpl:23 158 | qt422016.ReleaseByteBuffer(qb422016) 159 | //line template/code.qtpl:23 160 | return qs422016 161 | //line template/code.qtpl:23 162 | } 163 | 164 | // The code css. 165 | 166 | //line template/code.qtpl:26 167 | func streamcode_css(qw422016 *qt422016.Writer) { 168 | //line template/code.qtpl:26 169 | qw422016.N().S(` 170 | 175 | `) 176 | //line template/code.qtpl:32 177 | } 178 | 179 | //line template/code.qtpl:32 180 | func writecode_css(qq422016 qtio422016.Writer) { 181 | //line template/code.qtpl:32 182 | qw422016 := qt422016.AcquireWriter(qq422016) 183 | //line template/code.qtpl:32 184 | streamcode_css(qw422016) 185 | //line template/code.qtpl:32 186 | qt422016.ReleaseWriter(qw422016) 187 | //line template/code.qtpl:32 188 | } 189 | 190 | //line template/code.qtpl:32 191 | func code_css() string { 192 | //line template/code.qtpl:32 193 | qb422016 := qt422016.AcquireByteBuffer() 194 | //line template/code.qtpl:32 195 | writecode_css(qb422016) 196 | //line template/code.qtpl:32 197 | qs422016 := string(qb422016.B) 198 | //line template/code.qtpl:32 199 | qt422016.ReleaseByteBuffer(qb422016) 200 | //line template/code.qtpl:32 201 | return qs422016 202 | //line template/code.qtpl:32 203 | } 204 | 205 | // The code contents. 206 | 207 | //line template/code.qtpl:35 208 | func streamcode_contents(qw422016 *qt422016.Writer, lang, data string) { 209 | //line template/code.qtpl:35 210 | qw422016.N().S(` 211 |
`)
216 | //line template/code.qtpl:36
217 | 	qw422016.E().S(data)
218 | //line template/code.qtpl:36
219 | 	qw422016.N().S(`
220 | `) 221 | //line template/code.qtpl:37 222 | } 223 | 224 | //line template/code.qtpl:37 225 | func writecode_contents(qq422016 qtio422016.Writer, lang, data string) { 226 | //line template/code.qtpl:37 227 | qw422016 := qt422016.AcquireWriter(qq422016) 228 | //line template/code.qtpl:37 229 | streamcode_contents(qw422016, lang, data) 230 | //line template/code.qtpl:37 231 | qt422016.ReleaseWriter(qw422016) 232 | //line template/code.qtpl:37 233 | } 234 | 235 | //line template/code.qtpl:37 236 | func code_contents(lang, data string) string { 237 | //line template/code.qtpl:37 238 | qb422016 := qt422016.AcquireByteBuffer() 239 | //line template/code.qtpl:37 240 | writecode_contents(qb422016, lang, data) 241 | //line template/code.qtpl:37 242 | qs422016 := string(qb422016.B) 243 | //line template/code.qtpl:37 244 | qt422016.ReleaseByteBuffer(qb422016) 245 | //line template/code.qtpl:37 246 | return qs422016 247 | //line template/code.qtpl:37 248 | } 249 | -------------------------------------------------------------------------------- /template/index.qtpl: -------------------------------------------------------------------------------- 1 | Index's contents. 2 | {% func index_contents() %} 3 | {% stripspace %} 4 |

Burning Rubber Paste

5 |

Usage

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
Method - EndpointEffect
POST / data=foo
Pastebin foo
PUT /id data=foo
Write foo into /id. Collisions disallowed. If a POST id coincides with your PUT content, it will be overwritten.
GET /id
Read paste with ID "id"
GET /id/raw
Get the raw contents of paste with ID "id"
GET /id/lang
Read paste with ID "id", and highlight it as "lang"
36 | 37 |

Examples

38 | {% endstripspace %} 39 |
http -f https://brpaste.example.com data=@file.txt
40 | http -f https://brpaste.example.com data=abcd
41 | http -f PUT https://brpaste.example.com/myPaste data=contents
42 | http https://brpaste.example.com/some_id/raw
43 | xdg-open https://brpaste.example.com/some_id/cpp
44 | {% stripspace %} 45 | 46 |

Paste from a browser

47 |
48 | 49 |
50 | 51 |
52 | {% endstripspace %} 53 | {% endfunc %} 54 | 55 | The index layout. 56 | {% func Index() %} 57 | {%= layout("", "", "", index_contents(), "") %} 58 | {% endfunc %} 59 | -------------------------------------------------------------------------------- /template/index.qtpl.go: -------------------------------------------------------------------------------- 1 | // Code generated by qtc from "index.qtpl". DO NOT EDIT. 2 | // See https://github.com/valyala/quicktemplate for details. 3 | 4 | // Index's contents. 5 | 6 | //line template/index.qtpl:2 7 | package template 8 | 9 | //line template/index.qtpl:2 10 | import ( 11 | qtio422016 "io" 12 | 13 | qt422016 "github.com/valyala/quicktemplate" 14 | ) 15 | 16 | //line template/index.qtpl:2 17 | var ( 18 | _ = qtio422016.Copy 19 | _ = qt422016.AcquireByteBuffer 20 | ) 21 | 22 | //line template/index.qtpl:2 23 | func streamindex_contents(qw422016 *qt422016.Writer) { 24 | //line template/index.qtpl:2 25 | qw422016.N().S(` 26 | `) 27 | //line template/index.qtpl:3 28 | qw422016.N().S(`

Burning Rubber Paste

Usage

Method - EndpointEffect
POST / data=foo
Pastebin foo
PUT /id data=foo
Write foo into /id. Collisions disallowed. If a POST id coincides with your PUT content, it will be overwritten.
GET /id
Read paste with ID "id"
GET /id/raw
Get the raw contents of paste with ID "id"
GET /id/lang
Read paste with ID "id", and highlight it as "lang"

Examples

`) 29 | //line template/index.qtpl:38 30 | qw422016.N().S(` 31 |
http -f https://brpaste.example.com data=@file.txt
 32 | http -f https://brpaste.example.com data=abcd
 33 | http -f PUT https://brpaste.example.com/myPaste data=contents
 34 | http https://brpaste.example.com/some_id/raw
 35 | xdg-open https://brpaste.example.com/some_id/cpp
36 | `) 37 | //line template/index.qtpl:44 38 | qw422016.N().S(`

Paste from a browser


`) 39 | //line template/index.qtpl:52 40 | qw422016.N().S(` 41 | `) 42 | //line template/index.qtpl:53 43 | } 44 | 45 | //line template/index.qtpl:53 46 | func writeindex_contents(qq422016 qtio422016.Writer) { 47 | //line template/index.qtpl:53 48 | qw422016 := qt422016.AcquireWriter(qq422016) 49 | //line template/index.qtpl:53 50 | streamindex_contents(qw422016) 51 | //line template/index.qtpl:53 52 | qt422016.ReleaseWriter(qw422016) 53 | //line template/index.qtpl:53 54 | } 55 | 56 | //line template/index.qtpl:53 57 | func index_contents() string { 58 | //line template/index.qtpl:53 59 | qb422016 := qt422016.AcquireByteBuffer() 60 | //line template/index.qtpl:53 61 | writeindex_contents(qb422016) 62 | //line template/index.qtpl:53 63 | qs422016 := string(qb422016.B) 64 | //line template/index.qtpl:53 65 | qt422016.ReleaseByteBuffer(qb422016) 66 | //line template/index.qtpl:53 67 | return qs422016 68 | //line template/index.qtpl:53 69 | } 70 | 71 | // The index layout. 72 | 73 | //line template/index.qtpl:56 74 | func StreamIndex(qw422016 *qt422016.Writer) { 75 | //line template/index.qtpl:56 76 | qw422016.N().S(` 77 | `) 78 | //line template/index.qtpl:57 79 | streamlayout(qw422016, "", "", "", index_contents(), "") 80 | //line template/index.qtpl:57 81 | qw422016.N().S(` 82 | `) 83 | //line template/index.qtpl:58 84 | } 85 | 86 | //line template/index.qtpl:58 87 | func WriteIndex(qq422016 qtio422016.Writer) { 88 | //line template/index.qtpl:58 89 | qw422016 := qt422016.AcquireWriter(qq422016) 90 | //line template/index.qtpl:58 91 | StreamIndex(qw422016) 92 | //line template/index.qtpl:58 93 | qt422016.ReleaseWriter(qw422016) 94 | //line template/index.qtpl:58 95 | } 96 | 97 | //line template/index.qtpl:58 98 | func Index() string { 99 | //line template/index.qtpl:58 100 | qb422016 := qt422016.AcquireByteBuffer() 101 | //line template/index.qtpl:58 102 | WriteIndex(qb422016) 103 | //line template/index.qtpl:58 104 | qs422016 := string(qb422016.B) 105 | //line template/index.qtpl:58 106 | qt422016.ReleaseByteBuffer(qb422016) 107 | //line template/index.qtpl:58 108 | return qs422016 109 | //line template/index.qtpl:58 110 | } 111 | -------------------------------------------------------------------------------- /template/layout.qtpl: -------------------------------------------------------------------------------- 1 | The main layout function. 2 | {% func layout(css, scripts, title, contents, bodyscripts string) %} 3 | 4 | {% stripspace %} 5 | 6 | 7 | 8 | 9 | 10 | {% if len(css) == 0 %} 11 | 24 | {% else %} 25 | {%s= css %} 26 | {% endif %} 27 | 28 | {% if len(scripts) != 0 %} 29 | {%s= scripts %} 30 | {% endif %} 31 | 32 | {% if len(title) == 0 %} 33 | Burning Rubber Paste 34 | {% else %} 35 | {%s= title %} 36 | {% endif %} 37 | 38 | 39 |
40 | {% if len(contents) != 0 %} 41 | {%s= contents %} 42 | {% endif %} 43 |
44 | {% if len(bodyscripts) != 0 %} 45 | {%s= bodyscripts %} 46 | {% endif %} 47 | 48 | 49 | {% endstripspace %} 50 | {% endfunc %} 51 | -------------------------------------------------------------------------------- /template/layout.qtpl.go: -------------------------------------------------------------------------------- 1 | // Code generated by qtc from "layout.qtpl". DO NOT EDIT. 2 | // See https://github.com/valyala/quicktemplate for details. 3 | 4 | // The main layout function. 5 | 6 | //line template/layout.qtpl:2 7 | package template 8 | 9 | //line template/layout.qtpl:2 10 | import ( 11 | qtio422016 "io" 12 | 13 | qt422016 "github.com/valyala/quicktemplate" 14 | ) 15 | 16 | //line template/layout.qtpl:2 17 | var ( 18 | _ = qtio422016.Copy 19 | _ = qt422016.AcquireByteBuffer 20 | ) 21 | 22 | //line template/layout.qtpl:2 23 | func streamlayout(qw422016 *qt422016.Writer, css, scripts, title, contents, bodyscripts string) { 24 | //line template/layout.qtpl:2 25 | qw422016.N().S(` 26 | 27 | `) 28 | //line template/layout.qtpl:4 29 | qw422016.N().S(``) 30 | //line template/layout.qtpl:10 31 | if len(css) == 0 { 32 | //line template/layout.qtpl:10 33 | qw422016.N().S(``) 34 | //line template/layout.qtpl:24 35 | } else { 36 | //line template/layout.qtpl:25 37 | qw422016.N().S(css) 38 | //line template/layout.qtpl:26 39 | } 40 | //line template/layout.qtpl:28 41 | if len(scripts) != 0 { 42 | //line template/layout.qtpl:29 43 | qw422016.N().S(scripts) 44 | //line template/layout.qtpl:30 45 | } 46 | //line template/layout.qtpl:32 47 | if len(title) == 0 { 48 | //line template/layout.qtpl:32 49 | qw422016.N().S(`Burning Rubber Paste`) 50 | //line template/layout.qtpl:34 51 | } else { 52 | //line template/layout.qtpl:35 53 | qw422016.N().S(title) 54 | //line template/layout.qtpl:36 55 | } 56 | //line template/layout.qtpl:36 57 | qw422016.N().S(`
`) 58 | //line template/layout.qtpl:40 59 | if len(contents) != 0 { 60 | //line template/layout.qtpl:41 61 | qw422016.N().S(contents) 62 | //line template/layout.qtpl:42 63 | } 64 | //line template/layout.qtpl:42 65 | qw422016.N().S(`
`) 66 | //line template/layout.qtpl:44 67 | if len(bodyscripts) != 0 { 68 | //line template/layout.qtpl:45 69 | qw422016.N().S(bodyscripts) 70 | //line template/layout.qtpl:46 71 | } 72 | //line template/layout.qtpl:46 73 | qw422016.N().S(``) 74 | //line template/layout.qtpl:49 75 | qw422016.N().S(` 76 | `) 77 | //line template/layout.qtpl:50 78 | } 79 | 80 | //line template/layout.qtpl:50 81 | func writelayout(qq422016 qtio422016.Writer, css, scripts, title, contents, bodyscripts string) { 82 | //line template/layout.qtpl:50 83 | qw422016 := qt422016.AcquireWriter(qq422016) 84 | //line template/layout.qtpl:50 85 | streamlayout(qw422016, css, scripts, title, contents, bodyscripts) 86 | //line template/layout.qtpl:50 87 | qt422016.ReleaseWriter(qw422016) 88 | //line template/layout.qtpl:50 89 | } 90 | 91 | //line template/layout.qtpl:50 92 | func layout(css, scripts, title, contents, bodyscripts string) string { 93 | //line template/layout.qtpl:50 94 | qb422016 := qt422016.AcquireByteBuffer() 95 | //line template/layout.qtpl:50 96 | writelayout(qb422016, css, scripts, title, contents, bodyscripts) 97 | //line template/layout.qtpl:50 98 | qs422016 := string(qb422016.B) 99 | //line template/layout.qtpl:50 100 | qt422016.ReleaseByteBuffer(qb422016) 101 | //line template/layout.qtpl:50 102 | return qs422016 103 | //line template/layout.qtpl:50 104 | } 105 | --------------------------------------------------------------------------------