commit 10847ca28a3ca9c4c1ae607b9192c46f4f188c75 Author: emilis Date: Fri Sep 2 15:42:48 2022 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bbc6377 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +target/ +**/*.rs.bk +Cargo.lock +bin/ +pkg/ +wasm-pack.log +__debug_bin +*.wasm +fupie +trash/ +fupie_wasm.js diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3f27cd0 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/main.go" + } + ] +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..58c1c15 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module sectorinf.com/emilis/fupie + +go 1.19 + +require github.com/stretchr/testify v1.8.0 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect + golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..18a3043 --- /dev/null +++ b/go.sum @@ -0,0 +1,19 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc= +golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/logie/logie.go b/logie/logie.go new file mode 100644 index 0000000..ade75b1 --- /dev/null +++ b/logie/logie.go @@ -0,0 +1,246 @@ +package logie + +import ( + "fmt" + "os" + "strings" + "time" +) + +const ( + printFmt = "%s%s %s\n" + escape byte = 0x1B + csi byte = '[' + separator byte = ';' + csiEnd byte = 'm' + fgColor byte = '3' + bgColor byte = '4' + reset byte = '0' +) + +type Maybe[T any] struct { + exists bool + value T +} + +func Exists[T any](t T) Maybe[T] { + return Maybe[T]{ + exists: true, + value: t, + } +} + +func Empty[T any]() Maybe[T] { + return Maybe[T]{} +} + +func (m Maybe[T]) Exists() bool { + return m.exists +} + +func (m Maybe[T]) Value() T { + return m.value +} + +type Color uint8 + +const ( + ColorBlack = '0' + ColorRed = '1' + ColorGreen = '2' + ColorYellow = '3' + ColorBlue = '4' + ColorMagenta = '5' + ColorCyan = '6' + ColorWhite = '7' +) + +type ColorSet struct { + fg Maybe[Color] + bg Maybe[Color] + font Maybe[Font] +} + +func Colors() ColorSet { + return ColorSet{} +} + +func (c ColorSet) Foreground(fg Color) ColorSet { + c.fg = Exists(fg) + return c +} + +func (c ColorSet) Background(bg Color) ColorSet { + c.bg = Exists(bg) + return c +} + +func (c ColorSet) Font(f Font) ColorSet { + c.font = Exists(f) + return c +} + +func (c ColorSet) String(v string) string { + var hasFg, hasBg, hasFont = c.fg.Exists(), c.bg.Exists(), c.font.Exists() + if !hasFg && !hasBg && !hasFont { + return v + } + // Build format + formatBytes := []byte{escape, csi} + if hasFg { + formatBytes = append(formatBytes, fgColor, byte(c.fg.Value())) + } + if hasBg { + if len(formatBytes) > 2 { + formatBytes = append(formatBytes, separator) + } + formatBytes = append(formatBytes, bgColor, byte(c.bg.Value())) + } + if hasFont { + if len(formatBytes) > 2 { + formatBytes = append(formatBytes, separator) + } + formatBytes = append(formatBytes, byte(c.font.Value())) + } + formatBytes = append(formatBytes, csiEnd) + formatBytes = append(append(formatBytes, []byte(v)...), escape, csi, reset, csiEnd) + return string(formatBytes) +} + +type Font uint8 + +const ( + FontBold = '1' + FontFaint = '2' + FontUnderlined = '4' + FontSlowBlink = '5' +) + +var ( + info = Colors().Font(FontFaint).String("[INFO]") + warn = Colors().Background(ColorYellow).String("[WARN]") + error_ = Colors().Foreground(ColorWhite).Background(ColorRed).Font(FontBold).String("[ERROR]") + fatal = Colors().Foreground(ColorRed).Background(ColorYellow).Font(FontBold).String("[FATAL]") +) + +type Log struct { + output *os.File + timeFmt string + color map[int]ColorSet + timeColor ColorSet +} + +func New() Log { + return Log{ + output: os.Stdout, + timeFmt: time.RFC822, + timeColor: Colors().Font(FontFaint), + color: map[int]ColorSet{}, + } +} + +func (l Log) Clone() Log { + colorClone := map[int]ColorSet{} + for k, cs := range l.color { + colorClone[k] = cs + } + l.color = colorClone + return l +} + +// ColorBracket returns a Log with the coloring information for the bracket index. +// +// The way coloring works in Logie is that you can only color the parts of your +// message that are within closed brackets `[]` in its format string +// (therefore non-f calls cannot color). The [index] being passed in is a zero-indexed +// index of which bracket set to color. +func (l Log) ColorBracket(index int, set ColorSet) Log { + l = l.Clone() + l.color[index] = set + + return l +} + +func (l Log) time() string { + return l.timeColor.String("[" + time.Now().Format(l.timeFmt) + "]") +} + +func (l Log) Logf(level, format string, args ...any) { + line := fmt.Sprintf(colorize(format, l.color), args...) + fmt.Fprintf(l.output, printFmt, l.time(), level, line) +} + +func colorize(format string, colors map[int]ColorSet) string { + var ( + parts = []string{} + bracketParts = []int{} + last int + ) +outer: + for index := 0; index < len(format); index++ { + if format[index] == '[' { + parts = append(parts, format[last:index]) + + for inner := index + 1; index < len(format); inner++ { + if format[inner] == ']' { + current := format[index : inner+1] + parts = append(parts, current) + bracketParts = append(bracketParts, len(parts)-1) + last = inner + 1 + continue outer + } + } + + parts = append(parts, format[index:]) + } + } + parts = append(parts, format[last:]) + + for bracketIndex, color := range colors { + if bracketIndex >= len(bracketParts) { + continue + } + parts[bracketParts[bracketIndex]] = color.String(parts[bracketParts[bracketIndex]]) + } + + return strings.Join(parts, "") +} + +func (l Log) Log(level string, args ...any) { + line := fmt.Sprint(args...) + fmt.Fprintf(l.output, printFmt, l.time(), level, line) +} + +func (l Log) Infof(format string, args ...any) { + l.Logf(info, format, args...) +} + +func (l Log) Info(args ...any) { + l.Log(info, args...) +} + +func (l Log) Warnf(format string, args ...any) { + l.Logf(warn, format, args...) +} + +func (l Log) Warn(args ...any) { + l.Log(warn, args...) +} + +func (l Log) Errorf(format string, args ...any) { + l.Logf(error_, format, args...) +} + +func (l Log) Error(args ...any) { + l.Log(error_, args...) +} + +func (l Log) Fatalf(format string, args ...any) { + l.Logf(fatal, format, args...) + os.Exit(1) +} + +func (l Log) Fatal(args ...any) { + l.Log(fatal, args...) + os.Exit(1) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..c6a7cde --- /dev/null +++ b/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "flag" + "net/http" + + "sectorinf.com/emilis/fupie/logie" + "sectorinf.com/emilis/fupie/ws" + "sectorinf.com/emilis/fupie/ws/html" + "sectorinf.com/emilis/fupie/ws/route" +) + +func main() { + var ( + uploadsPath = flag.String("path", ".", "Path to upload files to") + redirectPath = flag.String("redirect", "http://localhost:8008", "URI part to add the uploaded filename to for redirects") + host = flag.String("host", "127.0.0.1:8008", "Hostname to listen to") + ) + flag.Parse() + + log := logie.New() + router := route.New(log).Group("v1", func(r route.Router) route.Router { + upload, err := ws.Upload(*uploadsPath, *redirectPath, 1<<20) + if err != nil { + log.Fatal(err.Error()) + } + r.POST("upload", upload) + + return r + }). + GET("/", html.Index). + Middleware(ws.LoggingMiddleware(log)) + + log.ColorBracket(0, logie.Colors().Background(logie.ColorWhite).Foreground(logie.ColorBlack)).Infof("Starting server on [%s]", host) + log.Fatal(http.ListenAndServe(*host, router)) +} diff --git a/ws/html/index.go b/ws/html/index.go new file mode 100644 index 0000000..6916667 --- /dev/null +++ b/ws/html/index.go @@ -0,0 +1,24 @@ +package html + +import ( + "embed" + "html/template" + "net/http" + + "sectorinf.com/emilis/fupie/ws/route" +) + +//go:embed templates +var templates embed.FS + +func Index(ctx route.Context) { + tmpl, err := template.ParseFS(templates, "templates/index.html") + if err != nil { + panic(err) + } + if err := tmpl.Execute(ctx.Writer, struct{}{}); err != nil { + ctx.Log.Errorf("index.html template: %s", err.Error()) + ctx.Status(http.StatusInternalServerError) + return + } +} diff --git a/ws/html/templates/index.html b/ws/html/templates/index.html new file mode 100644 index 0000000..0001053 --- /dev/null +++ b/ws/html/templates/index.html @@ -0,0 +1,55 @@ + + + + + File Upload + + + + + +

file upload

+
+ +
+ + +
+ +
+ + + + diff --git a/ws/logmware.go b/ws/logmware.go new file mode 100644 index 0000000..6358e60 --- /dev/null +++ b/ws/logmware.go @@ -0,0 +1,44 @@ +package ws + +import ( + "net/http" + + "sectorinf.com/emilis/fupie/logie" + "sectorinf.com/emilis/fupie/ws/route" +) + +type LoggingSet struct { + log1XX logie.Log + log2XX logie.Log + log3XX logie.Log + log4XX logie.Log + log5XX logie.Log +} + +func LoggingMiddleware(l logie.Log) route.Middleware { + l = l.ColorBracket(1, logie.Colors().Font(logie.FontFaint)) + log := LoggingSet{ + log1XX: l.ColorBracket(0, logie.Colors().Font(logie.FontFaint)), + log2XX: l.ColorBracket(0, logie.Colors().Foreground(logie.ColorWhite).Background(logie.ColorGreen)), + log3XX: l.ColorBracket(0, logie.Colors().Foreground(logie.ColorYellow).Font(logie.FontFaint)), + log4XX: l.ColorBracket(0, logie.Colors().Foreground(logie.ColorWhite).Background(logie.ColorMagenta)), + log5XX: l.ColorBracket(0, logie.Colors().Foreground(logie.ColorWhite).Background(logie.ColorRed).Font(logie.FontBold)), + } + return func(req http.Request, keep route.Keeper) { + var logger logie.Log + switch status := keep.Status; { + case status > 199 && status < 300: + logger = log.log2XX + case status > 299 && status < 400: + logger = log.log3XX + case status > 399 && status < 500: + logger = log.log4XX + case status > 499 && status < 600: + logger = log.log5XX + + default: + logger = log.log1XX + } + logger.Infof("[%d]: [%s]", keep.Status, req.URL.Path) + } +} diff --git a/ws/route/context.go b/ws/route/context.go new file mode 100644 index 0000000..c149273 --- /dev/null +++ b/ws/route/context.go @@ -0,0 +1,79 @@ +package route + +import ( + "context" + "encoding/json" + "net/http" + + "sectorinf.com/emilis/fupie/logie" +) + +type Keeper struct { + Status int +} + +type ResponseWriterKeeper struct { + http.ResponseWriter + Keeper +} + +func (r *ResponseWriterKeeper) WriteHeader(status int) { + r.Status = status + r.ResponseWriter.WriteHeader(status) +} + +type Context struct { + Writer http.ResponseWriter + Request *http.Request + Context context.Context + Log logie.Log +} + +func (c Context) JSON(v any) { + c.JSONStatus(http.StatusOK, v) +} + +func (c Context) JSONStatus(status int, v any) { + data, err := json.Marshal(v) + if err != nil { + c.Log.Errorf("json marshal: %s", err.Error()) + c.Writer.WriteHeader(http.StatusInternalServerError) + return + } + c.Writer.WriteHeader(status) + c.Writer.Header().Set("Content-Type", "application/json") + c.Writer.Write(data) +} + +func (c Context) Status(status int) { + c.Writer.WriteHeader(status) + c.Writer.Write([]byte(http.StatusText(status))) +} + +func (c Context) InternalServerError(args ...any) { + c.Log.Error(args...) + c.Writer.WriteHeader(http.StatusInternalServerError) +} + +func (c Context) InternalServerErrorf(format string, args ...any) { + c.Log.Errorf(format, args...) + c.Writer.WriteHeader(http.StatusInternalServerError) +} + +func (c Context) DataStatus(status int, data []byte) { + c.DataStatusType(status, http.DetectContentType(data), data) +} + +func (c Context) Data(data []byte) { + c.DataStatus(http.StatusOK, data) +} + +func (c Context) DataStatusType(status int, contentType string, data []byte) { + c.Writer.Header().Set("Content-Type", contentType) + c.Writer.WriteHeader(status) + c.Writer.Write(data) +} + +func (c Context) SeeOther(uri string) { + http.Redirect(c.Writer, c.Request, uri, http.StatusSeeOther) +} diff --git a/ws/route/pattern.go b/ws/route/pattern.go new file mode 100644 index 0000000..3cc5d05 --- /dev/null +++ b/ws/route/pattern.go @@ -0,0 +1,48 @@ +package route + +import "strings" + +func Pattern(v string) PartPattern { + return PartPattern(splitWithoutEmpty(v)) +} + +type PartPattern []string + +func (p PartPattern) String() string { + return strings.Join(p, "/") +} + +func (p PartPattern) Match(path string) bool { + pathParts := splitWithoutEmpty(path) + for i, part := range p { + if len(pathParts)-1 <= i { + return part == "*" || part == pathParts[i] + } + pathPart := pathParts[i] + if wildcardIndex := strings.IndexByte(part, '*'); wildcardIndex >= 0 { + if len(pathPart) < wildcardIndex || pathPart[:wildcardIndex] != part[:wildcardIndex] { + return false + } + } else if pathPart != part { + return false + } + } + return false +} + +func splitWithoutEmpty(v string) []string { + out := []string{} + start := 0 + for index, char := range v { + if char == '/' { + sub := v[start:index] + if len(sub) == 0 { + start++ + continue + } + out = append(out, sub) + start = index + 1 + } + } + return append(out, v[start:]) +} diff --git a/ws/route/pattern_test.go b/ws/route/pattern_test.go new file mode 100644 index 0000000..221c04b --- /dev/null +++ b/ws/route/pattern_test.go @@ -0,0 +1,56 @@ +package route_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "sectorinf.com/emilis/fupie/ws/route" +) + +func TestPattern(t *testing.T) { + tests := map[string]struct { + patternInput string + testPaths map[string]bool + }{ + "wildcard in middle of path": { + patternInput: "/hello/*/bye", + testPaths: map[string]bool{ + "/hello/hi/bye": true, + "/hello/no/bye": true, + "/hello/something/bye/something_else": false, + "/random/nothing/bye": false, + }, + }, + "wildcard within word": { + patternInput: "/hello/seeya-*/bye", + testPaths: map[string]bool{ + "/hello/seeya-hi/bye": true, + "/hello/seeya-no/bye": true, + "/hello/hi/bye": false, + "/hello/no/bye": false, + "/hello/something/bye/something_else": false, + "/random/nothing/bye": false, + "/hello/seeya-/bye": true, + }, + }, + "root /": { + patternInput: "/", + testPaths: map[string]bool{ + "": true, + "/": true, + "/hello/something/bye/something_else": false, + "/random/nothing/bye": false, + }, + }, + } + + for name, test := range tests { + test := test + t.Run(name, func(tt *testing.T) { + p := route.Pattern(test.patternInput) + for path, expected := range test.testPaths { + assert.Equal(t, expected, p.Match(path), path) + } + }) + } +} diff --git a/ws/route/router.go b/ws/route/router.go new file mode 100644 index 0000000..c9c13e0 --- /dev/null +++ b/ws/route/router.go @@ -0,0 +1,129 @@ +package route + +import ( + "context" + "fmt" + "net/http" + "strings" + + "sectorinf.com/emilis/fupie/logie" +) + +type Method string +type Route struct { + Pattern PartPattern + Handler HandlerFunc +} +type Routes []Route +type Middleware func(http.Request, Keeper) + +var ( + NotFoundHandler = func(ctx Context) { + ctx.Status(http.StatusNotFound) + } +) + +func (r Routes) Match(path string) HandlerFunc { + for _, route := range r { + if route.Pattern.Match(path) { + return route.Handler + } + } + return NotFoundHandler +} + +type HandlerFunc func(Context) + +type Router struct { + fnMatcher map[Method]Routes + middleware []Middleware + logger logie.Log +} + +func New(logger logie.Log) Router { + return Router{ + fnMatcher: map[Method]Routes{ + http.MethodGet: {}, + http.MethodConnect: {}, + http.MethodPatch: {}, + http.MethodPost: {}, + http.MethodPut: {}, + http.MethodDelete: {}, + http.MethodHead: {}, + http.MethodOptions: {}, + http.MethodTrace: {}, + }, + logger: logger, + middleware: []Middleware{}, + } +} + +func (r Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { + byMethod, exists := r.fnMatcher[Method(req.Method)] + if !exists { + w.WriteHeader(http.StatusNotFound) + return + } + fn := byMethod.Match(req.URL.Path) + rwk := &ResponseWriterKeeper{ + ResponseWriter: w, + Keeper: Keeper{ + // Default 200 + Status: http.StatusOK, + }, + } + fn(Context{ + Request: req, + Writer: rwk, + Context: context.Background(), + Log: r.logger, + }) + for _, mware := range r.middleware { + mware(*req, rwk.Keeper) + } +} + +func (r Router) With(method, pathExpr string, handler HandlerFunc) Router { + r.fnMatcher[Method(method)] = append(r.fnMatcher[Method(method)], Route{ + Pattern: Pattern(pathExpr), + Handler: handler, + }) + + return r +} + +func (r Router) GET(pathExpr string, handler HandlerFunc) Router { + return r.With(http.MethodGet, pathExpr, handler) +} + +func (r Router) POST(pathExpr string, handler HandlerFunc) Router { + return r.With(http.MethodPost, pathExpr, handler) +} + +func (r Router) PATCH(pathExpr string, handler HandlerFunc) Router { + return r.With(http.MethodPatch, pathExpr, handler) +} + +func (r Router) PUT(pathExpr string, handler HandlerFunc) Router { + return r.With(http.MethodPut, pathExpr, handler) +} + +func (r Router) DELETE(pathExpr string, handler HandlerFunc) Router { + return r.With(http.MethodDelete, pathExpr, handler) +} + +func (r Router) Middleware(mware ...Middleware) Router { + r.middleware = mware + return r +} + +func (r Router) Group(prefix string, fn func(Router) Router) Router { + prefix = strings.TrimSuffix(prefix, "/") + group := fn(New(r.logger)) + for method, byRoute := range group.fnMatcher { + for _, route := range byRoute { + r = r.With(string(method), fmt.Sprintf("%s/%s", prefix, route.Pattern.String()), route.Handler) + } + } + return r +} diff --git a/ws/upload.go b/ws/upload.go new file mode 100644 index 0000000..8e7cef3 --- /dev/null +++ b/ws/upload.go @@ -0,0 +1,78 @@ +package ws + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + + "sectorinf.com/emilis/fupie/ws/route" +) + +var ( + base64Stripper = strings.NewReplacer("/", "", "+", "", "=", "") +) + +func Upload(rootDir, redirect string, maxBytes int64) (route.HandlerFunc, error) { + stat, err := os.Stat(rootDir) + if err != nil { + return nil, fmt.Errorf("stat [%s]: %w", rootDir, err) + } + if !stat.IsDir() { + return nil, fmt.Errorf("root path [%s] is not a directory", rootDir) + } + rootDir, err = filepath.Abs(rootDir) + if err != nil { + return nil, fmt.Errorf("abs filepath [%s]: %w", rootDir, err) + } + redirect = strings.TrimRight(redirect, "/") + return func(ctx route.Context) { + if err := ctx.Request.ParseMultipartForm(maxBytes); err != nil { + ctx.Status(http.StatusBadRequest) + return + } + uploads := ctx.Request.MultipartForm.File["upload"] + if len(uploads) != 1 { + ctx.Status(http.StatusBadRequest) + return + } + header := uploads[0] + file, err := header.Open() + if err != nil { + ctx.InternalServerErrorf("header.Open: %s", err.Error()) + return + } + newFilename := rename(header.Filename) + path := filepath.Join(rootDir, newFilename) + fsFile, err := os.Create(path) + if err != nil { + ctx.InternalServerErrorf("creating file at [%s]: %s", path, err.Error()) + return + } + + if _, err := io.CopyBuffer(fsFile, file, nil); err != nil { + ctx.InternalServerErrorf("writing file at [%s]: %s", path, err.Error()) + return + } + fsFile.Close() + ctx.SeeOther(fmt.Sprintf("%s/%s", redirect, newFilename)) + }, nil +} + +func rename(name string) string { + return fmt.Sprintf("%s_%s", randName(4), name) +} + +func randName(bytes int) string { + buffer := make([]byte, bytes) + _, err := rand.Read(buffer) + if err != nil { + panic(err) + } + + return base64Stripper.Replace(base64.StdEncoding.EncodeToString(buffer)) +}