8 Commits

Author SHA1 Message Date
xuu
9ec281edea vulncheck
Some checks failed
Go Bump / bump (push) Successful in 40s
Go Test / test (push) Failing after 52s
2024-04-06 18:39:35 -06:00
xuu
0afc7a5bed fix: remove sigkill
Some checks failed
Go Test / test (push) Failing after 29s
Go Bump / bump (push) Successful in 31s
2024-02-29 11:02:00 -07:00
xuu
54fe38821c feat: handle file upload
Some checks failed
Go Test / test (push) Failing after 45s
Go Bump / bump (push) Successful in 23s
2024-02-13 09:27:26 -07:00
xuu
f9c064e948 refactor: move into package
Some checks failed
Go Test / test (push) Failing after 32s
Go Bump / bump (push) Successful in 34s
2024-01-22 16:05:13 -07:00
xuu
79114e8b8e Update .gitea/workflows/bump-push.yml
All checks were successful
Go Bump / bump (push) Successful in 28s
2023-11-09 13:52:20 -07:00
xuu
1b8e8ad26a chore: add favicon
All checks were successful
Go Bump / bump (push) Successful in 37s
2023-11-09 13:47:42 -07:00
xuu
f71e50fbe6 chore: fixes to paste ui 2023-11-09 11:11:22 -07:00
xuu
9a26239aa7 chore: add imgur api 2023-11-07 15:28:09 -07:00
30 changed files with 491 additions and 171 deletions

View File

@@ -20,6 +20,6 @@ jobs:
- run: go install github.com/psanetra/git-semver/cli@master - run: go install github.com/psanetra/git-semver/cli@master
- run: git tag v$(cli next --stable=false) && git push --tags || echo no change - run: git tag v$(cli next) && git push --tags || echo no change
- run: echo "🍏 This job's status is ${{ job.status }}." - run: echo "🍏 This job's status is ${{ job.status }}."

View File

@@ -1,8 +1,8 @@
name: Deploy name: Deploy
on: on:
push: # push:
branches: [ "master" ] # branches: [ "master" ]
release: release:
types: [ published ] types: [ published ]

View File

@@ -4,7 +4,7 @@ export PASTE_STORE=data/paste/
export ARTIFACT_STORE=data/artifact/ export ARTIFACT_STORE=data/artifact/
export IMAGE_STORE=data/image/ export IMAGE_STORE=data/image/
SOURCE=$(wildcard v2/*.go) $(wildcard v2/paste/*.go) $(wildcard assets/*.go) SOURCE=$(wildcard *.go) $(wildcard paste/*.go) $(wildcard assets/*.go)
ASSETS=$(wildcard assets/*) $(wildcard assets/public/*) $(wildcard assets/src/*) $(wildcard assets/src/paste/*) ASSETS=$(wildcard assets/*) $(wildcard assets/public/*) $(wildcard assets/src/*) $(wildcard assets/src/paste/*)
ASSET_FILE=assets/build/index.html ASSET_FILE=assets/build/index.html
BINARY=sour.is-paste BINARY=sour.is-paste
@@ -20,8 +20,7 @@ test:
go test ./... go test ./...
go vet ./... go vet ./...
run: $(BINARY) run: $(BINARY)
# go run ./v2 go run .
./$(BINARY)
build-assets: $(ASSET_FILE) build-assets: $(ASSET_FILE)
${ASSET_FILE}: $(ASSETS) ${ASSET_FILE}: $(ASSETS)
@@ -30,7 +29,7 @@ ${ASSET_FILE}: $(ASSETS)
build: $(BINARY) build: $(BINARY)
$(BINARY): $(SOURCE) $(ASSET_FILE) $(BINARY): $(SOURCE) $(ASSET_FILE)
go build -o $(BINARY) ./v2 go build -o $(BINARY) .
.PHONEY: all clean build run setup .PHONEY: all clean build run setup
# DO NOT DELETE # DO NOT DELETE

39
app.favicon.go Normal file
View File

@@ -0,0 +1,39 @@
package main
import (
"context"
"embed"
"io/fs"
"net/http"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/service"
)
var _ = apps.Register(10, func(ctx context.Context, svc *service.Harness) error {
_, span := lg.Span(ctx)
defer span.End()
svc.Add(&favicon{})
return nil
})
//go:embed favicon/*
var faviconAsset embed.FS
type favicon struct{}
func (favicon) RegisterHTTP(mux *http.ServeMux) {
dir, _ := fs.Sub(faviconAsset, "favicon")
srv := http.FileServer(http.FS(dir))
mux.Handle("/favicon.ico", srv)
mux.Handle("/favicon.txt", srv)
mux.Handle("/favicon-16x16.png", srv)
mux.Handle("/favicon-32x32.png", srv)
mux.Handle("/android-chrome-192x192.png", srv)
mux.Handle("/android-chrome-512x512.png", srv)
mux.Handle("/apple-touch-icon.png", srv)
mux.Handle("/site.webmanifest", srv)
}

View File

@@ -15,7 +15,7 @@ var _ = apps.Register(40, func(ctx context.Context, svc *service.Harness) error
store := env.Default("IMAGE_STORE", "data/") store := env.Default("IMAGE_STORE", "data/")
a, err := image.New(store, -1) a, err := image.New(ctx, store, -1)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -23,7 +23,7 @@ type info struct{}
func (info) RegisterHTTP(mux *http.ServeMux) { func (info) RegisterHTTP(mux *http.ServeMux) {
mux.HandleFunc("/app-info", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/app-info", func(w http.ResponseWriter, r *http.Request) {
name, version := service.AppName() name, version := service.AppName()
fmt.Fprint(w, name, version) fmt.Fprint(w, name, '@', version)
}) })
} }

View File

@@ -2,19 +2,12 @@ package main
import ( import (
"context" "context"
"crypto/rand"
"errors"
"fmt"
"log"
"net/http"
"strconv" "strconv"
"strings"
"go.sour.is/pkg/env" "go.sour.is/pkg/env"
"go.sour.is/pkg/lg" "go.sour.is/pkg/lg"
"go.sour.is/pkg/service" "go.sour.is/pkg/service"
"go.sour.is/paste/v2/assets"
"go.sour.is/paste/v2/paste" "go.sour.is/paste/v2/paste"
) )
@@ -28,104 +21,12 @@ var _ = apps.Register(50, func(ctx context.Context, svc *service.Harness) error
return err return err
} }
p, err := paste.New(store) srv, err := paste.NewService(store, randBytes)
if err != nil { if err != nil {
return err return err
} }
svc.Add(&pasteSRV{ svc.Add(srv)
ui: http.FileServer(http.FS(assets.GetUI())),
paste: p,
randBytes: int(randBytes),
})
return nil return nil
}) })
type pasteSRV struct {
ui http.Handler
paste *paste.Paste
randBytes int
}
func (a *pasteSRV) RegisterHTTP(mux *http.ServeMux) {
mux.Handle("/ui/", lg.Htrace(http.StripPrefix("/ui/", a.ui), "paste-assets"))
mux.Handle("/api", http.StripPrefix("/api", a))
mux.Handle("/api/", http.StripPrefix("/api/", a))
mux.Handle("/paste", http.StripPrefix("/paste", a))
mux.Handle("/paste/", http.StripPrefix("/paste/", a))
}
func (p *pasteSRV) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
switch {
case strings.HasPrefix(r.URL.Path, "rng"):
p.getRandom(w)
case strings.HasPrefix(r.URL.Path, "get"), r.URL.Path != "":
p.getPaste(w, strings.TrimPrefix(r.URL.Path, "get/"))
default:
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
}
case http.MethodPost:
switch {
case r.URL.Path == "":
p.postPaste(w, r)
default:
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
}
default:
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
}
}
func (p *pasteSRV) getRandom(w http.ResponseWriter) {
log.Println("get random")
s := make([]byte, p.randBytes)
_, _ = rand.Read(s)
w.Header().Set("content-type", "application/octet-stream")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(s)
}
func (p *pasteSRV) getPaste(w http.ResponseWriter, id string) {
log.Println("get paste", id)
w.Header().Set("content-type", "application/octet-stream")
err := p.paste.Get(w, id)
if err != nil {
switch {
case errors.Is(err, paste.ErrGone):
w.WriteHeader(http.StatusGone)
case errors.Is(err, paste.ErrNotFound):
w.WriteHeader(http.StatusNotFound)
case errors.Is(err, paste.ErrReadingContent):
w.WriteHeader(http.StatusInternalServerError)
}
fmt.Fprintf(w, "ERR %s", err)
return
}
}
func (p *pasteSRV) postPaste(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/octet-stream")
id, err := p.paste.Set(r.Body)
if err != nil {
fmt.Fprintf(w, "ERR %s", err)
return
}
log.Println("post paste", id)
fmt.Fprint(w, "OK ", id)
}

View File

@@ -1,13 +1,13 @@
{ {
"files": { "files": {
"main.css": "/ui/static/css/main.d4025777.css", "main.css": "/ui/static/css/main.d4025777.css",
"main.js": "/ui/static/js/main.1d06557a.js", "main.js": "/ui/static/js/main.939bead5.js",
"index.html": "/ui/index.html", "index.html": "/ui/index.html",
"main.d4025777.css.map": "/ui/static/css/main.d4025777.css.map", "main.d4025777.css.map": "/ui/static/css/main.d4025777.css.map",
"main.1d06557a.js.map": "/ui/static/js/main.1d06557a.js.map" "main.939bead5.js.map": "/ui/static/js/main.939bead5.js.map"
}, },
"entrypoints": [ "entrypoints": [
"static/css/main.d4025777.css", "static/css/main.d4025777.css",
"static/js/main.1d06557a.js" "static/js/main.939bead5.js"
] ]
} }

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="theme-color" content="#000000"><link rel="manifest" href="/ui/manifest.json"><link rel="shortcut icon" href="/ui/favicon.ico"><title>DN42 Paste</title><script defer="defer" src="/ui/static/js/main.1d06557a.js"></script><link href="/ui/static/css/main.d4025777.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html> <!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="theme-color" content="#000000"><link rel="manifest" href="/ui/manifest.json"><link rel="shortcut icon" href="/ui/favicon.ico"><title>DN42 Paste</title><script defer="defer" src="/ui/static/js/main.939bead5.js"></script><link href="/ui/static/css/main.d4025777.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

View File

@@ -200,17 +200,17 @@ class Paste extends Component {
onSubmit(event) { onSubmit(event) {
event.preventDefault(); event.preventDefault();
const { plain } = this.state; const { plain } = this.state;
const { history } = this.props; // const { history } = this.props;
this.encrypt(plain).then(([hash, decryptKey]) => { history.push('/#/' + hash + '!' + decryptKey) }); this.encrypt(plain).then(([hash, decryptKey]) => { history.pushState({}, '', '/ui/#/' + hash + '!' + decryptKey) });
} }
onNew(event) { onNew(event) {
const { history } = this.props; // const { history } = this.props;
this.setState({cipher:"", plain:"", hash: "", decryptKey: "", syntax: "text", expire: 604800, burn: false}, () => history.push('/')); this.setState({cipher:"", plain:"", hash: "", decryptKey: "", syntax: "text", expire: 604800, burn: false}, () => history.pushState({}, '', '/ui'));
} }
onCopy(event) { onCopy(event) {
const { history } = this.props; // const { history } = this.props;
this.setState({hash: "", decryptKey: ""}, () => history.push('/')) this.setState({hash: "", decryptKey: ""}, () => history.pushState({}, '', '/ui'))
} }
decrypt(tx) { decrypt(tx) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
favicon/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

BIN
favicon/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

BIN
favicon/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

6
favicon/favicon.txt Normal file
View File

@@ -0,0 +1,6 @@
This favicon was generated using the following graphics from Twitter Twemoji:
- Graphics Title: 1f4cb.svg
- Graphics Author: Copyright 2020 Twitter, Inc and other contributors (https://github.com/twitter/twemoji)
- Graphics Source: https://github.com/twitter/twemoji/blob/master/assets/svg/1f4cb.svg
- Graphics License: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/)

1
favicon/site.webmanifest Normal file
View File

@@ -0,0 +1 @@
{"name":"sour.is paste","short_name":"paste","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

15
go.mod
View File

@@ -3,14 +3,15 @@ module go.sour.is/paste/v2
go 1.21 go 1.21
require ( require (
github.com/gomarkdown/markdown v0.0.0-20200824053859-8c8b3816f167 github.com/gomarkdown/markdown v0.0.0-20230922105210-14b16010c2ee
github.com/h2non/filetype v1.1.0 github.com/h2non/filetype v1.1.0
github.com/matryer/is v1.4.1
github.com/rs/cors v1.6.0 github.com/rs/cors v1.6.0
go.opentelemetry.io/otel v1.18.0 go.opentelemetry.io/otel v1.18.0
go.opentelemetry.io/otel/trace v1.18.0 go.opentelemetry.io/otel/trace v1.18.0
go.sour.is/paste v0.0.0-20231022150928-96338f2f4441 go.sour.is/paste v0.0.0-20231022150928-96338f2f4441
go.sour.is/pkg v0.0.6 go.sour.is/pkg v0.0.6
golang.org/x/sys v0.13.0 golang.org/x/sys v0.18.0
sour.is/x/toolbox v0.12.17 sour.is/x/toolbox v0.12.17
) )
@@ -47,18 +48,18 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.41.0 // indirect go.opentelemetry.io/otel/exporters/prometheus v0.41.0 // indirect
go.opentelemetry.io/otel/metric v1.18.0 // indirect go.opentelemetry.io/otel/metric v1.18.0
go.opentelemetry.io/otel/sdk v1.18.0 // indirect go.opentelemetry.io/otel/sdk v1.18.0 // indirect
go.opentelemetry.io/otel/sdk/metric v0.41.0 // indirect go.opentelemetry.io/otel/sdk/metric v0.41.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.17.0 // indirect golang.org/x/net v0.23.0 // indirect
golang.org/x/sync v0.3.0 // indirect golang.org/x/sync v0.3.0 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
google.golang.org/grpc v1.58.0 // indirect google.golang.org/grpc v1.58.3 // indirect
google.golang.org/protobuf v1.31.0 // indirect google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/ini.v1 v1.57.0 // indirect gopkg.in/ini.v1 v1.57.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
) )

12
go.sum
View File

@@ -94,6 +94,8 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/gomarkdown/markdown v0.0.0-20200824053859-8c8b3816f167 h1:LP/6EfrZ/LyCc+SXvANDrIJ4sP9u2NAtqyv6QknetNQ= github.com/gomarkdown/markdown v0.0.0-20200824053859-8c8b3816f167 h1:LP/6EfrZ/LyCc+SXvANDrIJ4sP9u2NAtqyv6QknetNQ=
github.com/gomarkdown/markdown v0.0.0-20200824053859-8c8b3816f167/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU= github.com/gomarkdown/markdown v0.0.0-20200824053859-8c8b3816f167/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
github.com/gomarkdown/markdown v0.0.0-20230922105210-14b16010c2ee h1:gvsnG+uIVkOue7HrYAG2ZnOdLoJTqsLyuBFJaU0kX4M=
github.com/gomarkdown/markdown v0.0.0-20230922105210-14b16010c2ee/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -358,6 +360,8 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -385,11 +389,15 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -443,10 +451,14 @@ google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ij
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.58.0 h1:32JY8YpPMSR45K+c3o6b8VL73V+rR8k+DeMIr4vRH8o= google.golang.org/grpc v1.58.0 h1:32JY8YpPMSR45K+c3o6b8VL73V+rR8k+DeMIr4vRH8o=
google.golang.org/grpc v1.58.0/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/grpc v1.58.0/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=
google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -13,9 +13,11 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/h2non/filetype" "github.com/h2non/filetype"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
@@ -26,11 +28,15 @@ import (
type image struct { type image struct {
store string store string
maxSize int64 maxSize int64
m_image_get metric.Int64Counter
m_image_post metric.Int64Counter
m_image_error metric.Int64Counter
} }
const DefaultMaxSize = 500 * 1024 * 1024 const DefaultMaxSize = 500 * 1024 * 1024
func New(store string, maxSize int64) (a *image, err error) { func New(ctx context.Context, store string, maxSize int64) (a *image, err error) {
a = &image{ a = &image{
store: store, store: store,
maxSize: DefaultMaxSize, maxSize: DefaultMaxSize,
@@ -44,13 +50,30 @@ func New(store string, maxSize int64) (a *image, err error) {
return nil, fmt.Errorf("image Store location [%s] does not exist or is not writable", a.store) return nil, fmt.Errorf("image Store location [%s] does not exist or is not writable", a.store)
} }
return a, nil m := lg.Meter(ctx)
var merr error
a.m_image_get, merr = m.Int64Counter("m_image_get",
metric.WithDescription("retrieve image from store"),
)
err = errors.Join(err, merr)
a.m_image_post, merr = m.Int64Counter("m_image_post",
metric.WithDescription("save image to store"),
)
err = errors.Join(err, merr)
a.m_image_error, merr = m.Int64Counter("m_image_error",
metric.WithDescription("image api error"),
)
err = errors.Join(err, merr)
return a, err
} }
func (a *image) RegisterHTTP(mux *http.ServeMux) { func (a *image) RegisterHTTP(mux *http.ServeMux) {
mux.Handle("/i", http.StripPrefix("/i", a)) mux.Handle("/i", http.StripPrefix("/i", a))
mux.Handle("/i/", http.StripPrefix("/i/", a)) mux.Handle("/i/", http.StripPrefix("/i/", a))
mux.Handle("3/upload", http.StripPrefix("/3/upload", a)) mux.Handle("/3/upload", a)
} }
func (a *image) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (a *image) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@@ -58,53 +81,111 @@ func (a *image) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer span.End() defer span.End()
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet, http.MethodHead:
name := strings.TrimPrefix(r.URL.Path, "/") a.m_image_get.Add(ctx, 1)
name := strings.TrimPrefix(r.URL.Path, "/")
if name != "" {
rdr, head, err := a.loadFile(ctx, name)
if err != nil {
a.m_image_error.Add(ctx, 1)
writeError(w, err)
span.RecordError(err)
return
}
defer rdr.Close()
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Accept-Ranges", "bytes")
w.Header().Set("Content-Type", head.Mime)
w.Header().Set("ETag", head.ETag)
w.Header().Set("Last-Modified", head.Modified.Format(time.RFC850))
if r.Method == http.MethodHead {
return
}
// io.Copy(w, rdr)
http.ServeContent(w, r, "", head.Modified, rdr)
return
}
a.get(ctx, w, name)
case http.MethodPost: case http.MethodPost:
a.m_image_post.Add(ctx, 1)
var err error
defer r.Body.Close()
// var buf bytes.Buffer
// r.Body = io.NopCloser(io.TeeReader(r.Body, &buf))
var fd io.ReadCloser = r.Body
if r.URL.Path == "/3/upload" {
span.AddEvent("Imgur Emulation")
if data := r.FormValue("image"); data != "" {
var rdr io.Reader = strings.NewReader(data)
if t := r.FormValue("type"); t == "base64" {
rdr = base64.NewDecoder(base64.StdEncoding, rdr)
}
fd = io.NopCloser(rdr)
} else if mp, hd, err := r.FormFile("image"); err == nil {
defer mp.Close()
span.AddEvent(fmt.Sprint(hd))
fd = mp
} else if mp, hd, err := r.FormFile("file"); err == nil {
defer mp.Close()
span.AddEvent(fmt.Sprint(hd))
fd = mp
} else if err != nil {
span.RecordError(err)
w.WriteHeader(http.StatusBadRequest)
return
}
} else {
w.WriteHeader(http.StatusCreated)
}
length := 0 length := 0
if h := r.Header.Get("Content-Length"); h != "" { if h := r.Header.Get("Content-Length"); h != "" {
if i, err := strconv.Atoi(h); err != nil { if i, err := strconv.Atoi(h); err != nil {
length = i length = i
} }
} }
id, err := a.put(ctx, w, r.Body, length) id, err := a.put(ctx, fd, length)
switch { if err != nil {
case errors.Is(err, ErrGone): a.m_image_error.Add(ctx, 1)
w.WriteHeader(http.StatusGone) writeError(w, err)
case errors.Is(err, ErrNotFound): span.RecordError(err)
w.WriteHeader(http.StatusNotFound) return
case errors.Is(err, ErrReadingContent):
w.WriteHeader(http.StatusInternalServerError)
case errors.Is(err, ErrUnsupportedType):
w.WriteHeader(http.StatusUnsupportedMediaType)
} }
type data struct{ type data struct {
Link string `json:"link"` Link string `json:"link"`
DeleteHash string `json:"deletehash"` DeleteHash string `json:"deletehash"`
} }
var resp = struct{ var resp = struct {
Data data `json:"data"` Data data `json:"data"`
Success bool `json:"success"` Success bool `json:"success"`
Status int `json:"status"` Status int `json:"status"`
}{ }{
Data: data{ Data: data{
Link: fmt.Sprintf("https://%s/%s", r.Host, id), Link: fmt.Sprintf("https://%s/i/%s", r.Host, id),
}, },
Success: true, Success: true,
Status: 200, Status: 200,
} }
json.NewEncoder(w).Encode(resp) err = json.NewEncoder(w).Encode(resp)
span.RecordError(err)
default: default:
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
} }
} }
func (a *image) get(ctx context.Context, w http.ResponseWriter, name string) error { func (a *image) loadFile(ctx context.Context, name string) (io.ReadSeekCloser, *Header, error) {
_, span := lg.Span(ctx) _, span := lg.Span(ctx)
defer span.End() defer span.End()
@@ -114,38 +195,44 @@ func (a *image) get(ctx context.Context, w http.ResponseWriter, name string) err
fname := filepath.Join(a.store, id) fname := filepath.Join(a.store, id)
if !chkFile(fname) { if !chkFile(fname) {
return fmt.Errorf("%w: %s", ErrNotFound, fname) return nil, nil, fmt.Errorf("%w: %s", ErrNotFound, fname)
} }
if chkGone(fname) { if chkGone(fname) {
return fmt.Errorf("%w: %s", ErrGone, fname) return nil, nil, fmt.Errorf("%w: %s", ErrGone, fname)
} }
f, err := os.Open(fname) f, err := os.Open(fname)
if err != nil { if err != nil {
return err return nil, nil, err
}
stat, err := f.Stat()
if err != nil {
return nil, nil, err
} }
defer f.Close()
pr := readutil.NewPreviewReader(f) pr := readutil.NewPreviewReader(f)
mime, err := readutil.ReadMIME(pr, name) mime, err := readutil.ReadMIME(pr, name)
if err != nil { if err != nil {
return err return nil, nil, err
} }
w.Header().Set("Content-Type", mime)
w.Header().Set("X-Content-Type-Options", "nosniff")
_, _ = io.Copy(w, pr.Drain()) f.Seek(0, 0)
return nil
return f,
&Header{Mime: mime, Modified: stat.ModTime(), ETag: name},
nil
} }
func (a *image) put(ctx context.Context, w http.ResponseWriter, r io.ReadCloser, length int) (string, error) { func (a *image) put(ctx context.Context, r io.ReadCloser, length int) (string, error) {
_, span := lg.Span(ctx) _, span := lg.Span(ctx)
defer span.End() defer span.End()
defer r.Close() defer r.Close()
span.AddEvent("content length", trace.WithAttributes(attribute.Int64("max-size", a.maxSize), attribute.Int("length", length)))
if length > 0 { if length > 0 {
if int64(length) > a.maxSize { if int64(length) > a.maxSize {
return "", ErrSizeTooLarge return "", ErrSizeTooLarge
@@ -155,6 +242,7 @@ func (a *image) put(ctx context.Context, w http.ResponseWriter, r io.ReadCloser,
rdr := io.LimitReader(r, a.maxSize) rdr := io.LimitReader(r, a.maxSize)
pr := readutil.NewPreviewReader(rdr) pr := readutil.NewPreviewReader(rdr)
if !isImageOrVideo(pr) { if !isImageOrVideo(pr) {
span.AddEvent("not image")
return "", ErrUnsupportedType return "", ErrUnsupportedType
} }
rdr = pr.Drain() rdr = pr.Drain()
@@ -162,6 +250,7 @@ func (a *image) put(ctx context.Context, w http.ResponseWriter, r io.ReadCloser,
s256 := sha256.New() s256 := sha256.New()
tmp, err := os.CreateTemp(a.store, "image-") tmp, err := os.CreateTemp(a.store, "image-")
if err != nil { if err != nil {
span.RecordError(err)
return "", fmt.Errorf("%w: %w", ErrBadInput, err) return "", fmt.Errorf("%w: %w", ErrBadInput, err)
} }
@@ -169,6 +258,7 @@ func (a *image) put(ctx context.Context, w http.ResponseWriter, r io.ReadCloser,
m := io.MultiWriter(s256, tmp) m := io.MultiWriter(s256, tmp)
if _, err := io.Copy(m, rdr); err != nil { if _, err := io.Copy(m, rdr); err != nil {
span.RecordError(err)
return "", fmt.Errorf("%w: %w", ErrBadInput, err) return "", fmt.Errorf("%w: %w", ErrBadInput, err)
} }
tmp.Close() tmp.Close()
@@ -176,7 +266,7 @@ func (a *image) put(ctx context.Context, w http.ResponseWriter, r io.ReadCloser,
id := base64.RawURLEncoding.EncodeToString(s256.Sum(nil)[12:]) id := base64.RawURLEncoding.EncodeToString(s256.Sum(nil)[12:])
fname := filepath.Join(a.store, id) fname := filepath.Join(a.store, id)
span.AddEvent("image: moving file", trace.WithAttributes(attribute.String("src", tmp.Name()), attribute.String("dst", fname))) span.AddEvent("image: moving file", trace.WithAttributes(attribute.String("image-src", tmp.Name()), attribute.String("image-dst", fname)))
_ = os.Rename(tmp.Name(), fname) _ = os.Rename(tmp.Name(), fname)
return id, nil return id, nil
@@ -245,6 +335,24 @@ func chkGone(path string) bool {
return false return false
} }
func writeError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, ErrGone):
w.WriteHeader(http.StatusGone)
case errors.Is(err, ErrNotFound):
w.WriteHeader(http.StatusNotFound)
case errors.Is(err, ErrReadingContent):
w.WriteHeader(http.StatusInternalServerError)
case errors.Is(err, ErrUnsupportedType):
w.WriteHeader(http.StatusUnsupportedMediaType)
}
}
type Header struct {
Mime string
Modified time.Time
ETag string
}
var ( var (
ErrNotFound = errors.New("not found") ErrNotFound = errors.New("not found")

118
image/image_test.go Normal file
View File

@@ -0,0 +1,118 @@
package image_test
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/matryer/is"
"go.sour.is/paste/v2/image"
)
var testImage = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg=="
func TestPostImgur(t *testing.T) {
is := is.New(t)
ctx := context.Background()
dir, err := os.MkdirTemp("", "image")
is.NoErr(err)
im, err := image.New(ctx, dir, 0)
is.NoErr(err)
{
body := &bytes.Buffer{}
wr := multipart.NewWriter(body)
w, err := wr.CreateFormField("type")
is.NoErr(err)
fmt.Fprint(w, "base64")
w, err = wr.CreateFormField("image")
is.NoErr(err)
fmt.Fprint(w, testImage)
err = wr.Close()
is.NoErr(err)
t.Log(body.String())
req := httptest.NewRequest("POST", "/3/upload", body)
req.Header.Set("Content-Type", "multipart/form-data; boundary="+wr.Boundary())
res := httptest.NewRecorder()
im.ServeHTTP(res, req)
is.Equal(res.Code, http.StatusOK)
is.Equal(res.Body.String(), `{"data":{"link":"https://example.com/i/Igg59VTi6JNuXVriXMR3U_lzfAc","deletehash":""},"success":true,"status":200}
`)
}
{
req := httptest.NewRequest("GET", "/Igg59VTi6JNuXVriXMR3U_lzfAc", nil)
res := httptest.NewRecorder()
im.ServeHTTP(res, req)
is.Equal(res.Code, http.StatusOK)
s := base64.StdEncoding.EncodeToString(res.Body.Bytes())
is.Equal(s, testImage)
t.Log(res.Header())
is.Equal(res.Header().Get("ETag"), "Igg59VTi6JNuXVriXMR3U_lzfAc")
}
err = os.RemoveAll(dir)
is.NoErr(err)
}
func TestPost(t *testing.T) {
is := is.New(t)
ctx := context.Background()
dir, err := os.MkdirTemp("", "image")
is.NoErr(err)
im, err := image.New(ctx, dir, 0)
is.NoErr(err)
{
req := httptest.NewRequest("POST", "/i", base64.NewDecoder(base64.StdEncoding, strings.NewReader(testImage)))
res := httptest.NewRecorder()
im.ServeHTTP(res, req)
is.Equal(res.Code, http.StatusCreated)
is.Equal(res.Body.String(), `{"data":{"link":"https://example.com/i/Igg59VTi6JNuXVriXMR3U_lzfAc","deletehash":""},"success":true,"status":200}
`)
}
{
req := httptest.NewRequest("GET", "/Igg59VTi6JNuXVriXMR3U_lzfAc", nil)
res := httptest.NewRecorder()
im.ServeHTTP(res, req)
is.Equal(res.Code, http.StatusOK)
s := base64.StdEncoding.EncodeToString(res.Body.Bytes())
is.Equal(s, testImage)
}
err = os.RemoveAll(dir)
is.NoErr(err)
}

View File

@@ -18,7 +18,7 @@ var apps service.Apps
var appName, version = service.AppName() var appName, version = service.AppName()
func main() { func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
go func() { go func() {
<-ctx.Done() <-ctx.Done()
defer cancel() // restore interrupt function defer cancel() // restore interrupt function

19
paste/public/index.html Normal file
View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="manifest" href="/ui/manifest.json">
<link rel="shortcut icon" href="/ui/favicon.ico">
<title>DN42 Paste</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link href="paste.css" rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
</body>
</html>

0
paste/public/paste.css Normal file
View File

114
paste/service.go Normal file
View File

@@ -0,0 +1,114 @@
package paste
import (
"crypto/rand"
"errors"
"fmt"
"log"
"net/http"
"strings"
"go.sour.is/paste/v2/assets"
"go.sour.is/pkg/lg"
)
type service struct {
ui http.Handler
paste *Paste
randBytes int
}
func NewService(store string, randBytes int64) (*service, error) {
p, err := New(store)
if err != nil {
return nil, err
}
return &service{
ui: http.FileServer(http.FS(assets.GetUI())),
paste: p,
randBytes: int(randBytes),
}, nil
}
func (a *service) RegisterHTTP(mux *http.ServeMux) {
mux.Handle("/ui/", lg.Htrace(http.StripPrefix("/ui/", a.ui), "paste-assets"))
mux.Handle("/api", http.StripPrefix("/api", a))
mux.Handle("/api/", http.StripPrefix("/api/", a))
mux.Handle("/paste", http.StripPrefix("/paste", a))
mux.Handle("/paste/", http.StripPrefix("/paste/", a))
}
func (p *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
switch {
case strings.HasPrefix(r.URL.Path, "rng"):
p.getRandom(w)
case strings.HasPrefix(r.URL.Path, "get"), r.URL.Path != "":
p.getPaste(w, strings.TrimPrefix(r.URL.Path, "get/"))
default:
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
}
case http.MethodPost:
switch {
case r.URL.Path == "":
p.postPaste(w, r)
default:
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
}
default:
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
}
}
func (p *service) getRandom(w http.ResponseWriter) {
log.Println("get random")
s := make([]byte, p.randBytes)
_, _ = rand.Read(s)
w.Header().Set("content-type", "application/octet-stream")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(s)
}
func (p *service) getPaste(w http.ResponseWriter, id string) {
log.Println("get paste", id)
w.Header().Set("content-type", "application/octet-stream")
err := p.paste.Get(w, id)
if err != nil {
switch {
case errors.Is(err, ErrGone):
w.WriteHeader(http.StatusGone)
case errors.Is(err, ErrNotFound):
w.WriteHeader(http.StatusNotFound)
case errors.Is(err, ErrReadingContent):
w.WriteHeader(http.StatusInternalServerError)
}
fmt.Fprintf(w, "ERR %s", err)
return
}
}
func (p *service) postPaste(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/octet-stream")
id, err := p.paste.Set(r.Body)
if err != nil {
fmt.Fprintf(w, "ERR %s", err)
return
}
log.Println("post paste", id)
fmt.Fprint(w, "OK ", id)
}

View File

@@ -20,11 +20,13 @@ var _ = apps.Register(20, func(ctx context.Context, svc *service.Harness) error
svc.Add(s) svc.Add(s)
mux := mux.New() mux := mux.New()
s.Handler = cors.AllowAll().Handler(mux)
s.Handler = cors.AllowAll().Handler(mux)
hdlr := s.Handler
s.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r.Method, r.URL.Path) log.Println(r.Method, r.URL.Path)
mux.ServeHTTP(w, r) hdlr.ServeHTTP(w, r)
}) })
s.Addr = env.Default("HTTP_LISTEN", ":8080") s.Addr = env.Default("HTTP_LISTEN", ":8080")