initial commit
This commit is contained in:
commit
10847ca28a
|
@ -0,0 +1,11 @@
|
|||
target/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
bin/
|
||||
pkg/
|
||||
wasm-pack.log
|
||||
__debug_bin
|
||||
*.wasm
|
||||
fupie
|
||||
trash/
|
||||
fupie_wasm.js
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>File Upload</title>
|
||||
<link rel="icon" type="image/gif" href="https://jigglypuff.zone/pufftaunt.gif">
|
||||
<style>
|
||||
body {
|
||||
background-color: black;
|
||||
color: rebeccapurple;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#main-upload {
|
||||
/* margin: auto; */
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 10%;
|
||||
margin-bottom: 30%;
|
||||
width: 30%;
|
||||
border: 5px solid rebeccapurple;
|
||||
height: 30vw;
|
||||
padding: 10px;
|
||||
background-color: rgba(13, 6, 19, 0.4);
|
||||
font-size: large;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>file upload</h1>
|
||||
<div id="main-upload">
|
||||
<!-- <div id="upload-box"> -->
|
||||
<form id="upload-form" method="post" action="/v1/upload" enctype="multipart/form-data">
|
||||
<label for="upload">Upload a file</label>
|
||||
<input type="file" id="upload" name="upload" accept="*" value="upload">
|
||||
</form>
|
||||
<!-- </div> -->
|
||||
</div>
|
||||
<script>
|
||||
window.addEventListener('paste', e => {
|
||||
document.getElementById("upload").files = e.clipboardData.files;
|
||||
document.getElementById("upload-form").submit();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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:])
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
Loading…
Reference in New Issue