chore: fixes to paste ui

This commit is contained in:
xuu 2023-11-08 13:44:28 -07:00
parent 9a26239aa7
commit f71e50fbe6
Signed by: xuu
GPG Key ID: 8B3B0604F164E04F
11 changed files with 237 additions and 58 deletions

View File

@ -3,7 +3,6 @@ export HTTP_LISTEN=:8085
export PASTE_STORE=data/paste/ 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/
export CGO_ENABLED=0
SOURCE=$(wildcard *.go) $(wildcard 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/*)
@ -21,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)

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) {

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.21
require ( require (
github.com/gomarkdown/markdown v0.0.0-20200824053859-8c8b3816f167 github.com/gomarkdown/markdown v0.0.0-20200824053859-8c8b3816f167
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

View File

@ -13,6 +13,7 @@ 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"
@ -58,63 +59,98 @@ 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, "/") name := strings.TrimPrefix(r.URL.Path, "/")
if name != "" {
a.get(ctx, w, name) rdr, head, err := a.loadFile(ctx, name)
case http.MethodPost:
var err error
var fd io.ReadCloser = r.Body
if r.URL.Path == "/3/upload" {
fd, _, err = r.FormFile("image")
if err != nil { if err != nil {
w.WriteHeader(http.StatusBadRequest) writeError(w, err)
span.RecordError(err)
return 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
} }
case http.MethodPost:
var err error
defer r.Body.Close()
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 fd, _, err = r.FormFile("image"); err != nil {
if err != nil {
span.RecordError(err)
w.WriteHeader(http.StatusBadRequest)
return
}
defer fd.Close()
}
w.WriteHeader(http.StatusOK)
} 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, fd, length) id, err := a.put(ctx, fd, length)
switch { if err != nil {
case errors.Is(err, ErrGone): writeError(w, err)
w.WriteHeader(http.StatusGone) span.RecordError(err)
case errors.Is(err, ErrNotFound): return
w.WriteHeader(http.StatusNotFound)
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/i/%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()
@ -124,38 +160,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
@ -165,6 +207,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()
@ -172,6 +215,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)
} }
@ -179,6 +223,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()
@ -186,7 +231,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
@ -255,6 +300,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")

115
image/image_test.go Normal file
View File

@ -0,0 +1,115 @@
package image_test
import (
"bytes"
"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)
dir, err := os.MkdirTemp("", "image")
is.NoErr(err)
im, err := image.New(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)
dir, err := os.MkdirTemp("", "image")
is.NoErr(err)
im, err := image.New(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

@ -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")