Compare commits
3 Commits
2cbd981902
...
1b8e8ad26a
Author | SHA1 | Date | |
---|---|---|---|
1b8e8ad26a | |||
f71e50fbe6 | |||
9a26239aa7 |
|
@ -1,8 +1,8 @@
|
||||||
name: Deploy
|
name: Deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
# push:
|
||||||
branches: [ "master" ]
|
# branches: [ "master" ]
|
||||||
release:
|
release:
|
||||||
types: [ published ]
|
types: [ published ]
|
||||||
|
|
||||||
|
|
7
Makefile
7
Makefile
|
@ -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
39
app.favicon.go
Normal 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)
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -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
File diff suppressed because one or more lines are too long
|
@ -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) {
|
||||||
|
|
BIN
favicon/android-chrome-192x192.png
Normal file
BIN
favicon/android-chrome-192x192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
BIN
favicon/android-chrome-512x512.png
Normal file
BIN
favicon/android-chrome-512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
BIN
favicon/apple-touch-icon.png
Normal file
BIN
favicon/apple-touch-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.6 KiB |
BIN
favicon/favicon-16x16.png
Normal file
BIN
favicon/favicon-16x16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 472 B |
BIN
favicon/favicon-32x32.png
Normal file
BIN
favicon/favicon-32x32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 847 B |
BIN
favicon/favicon.ico
Normal file
BIN
favicon/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
6
favicon/favicon.txt
Normal file
6
favicon/favicon.txt
Normal 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
1
favicon/site.webmanifest
Normal 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"}
|
3
go.mod
3
go.mod
|
@ -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
|
||||||
|
@ -47,7 +48,7 @@ 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
|
||||||
|
|
168
image/image.go
168
image/image.go
|
@ -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,31 @@ 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,27 +82,77 @@ 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 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, 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 {
|
||||||
|
@ -91,20 +165,21 @@ func (a *image) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
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 +189,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())
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *image) put(ctx context.Context, w http.ResponseWriter, r io.ReadCloser, length int) (string, error) {
|
f.Seek(0, 0)
|
||||||
|
|
||||||
|
return f,
|
||||||
|
&Header{Mime: mime, Modified: stat.ModTime(), ETag: name},
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +236,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 +244,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 +252,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 +260,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 +329,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
118
image/image_test.go
Normal 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)
|
||||||
|
}
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in New Issue
Block a user