initial commit
This commit is contained in:
commit
936340e9b9
|
@ -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")
|
||||||
|
)
|
||||||
|
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))
|
||||||
|
|
||||||
|
host := ":8008"
|
||||||
|
log.ColorBracket(0, logie.Colors().Background(logie.ColorWhite).Foreground(logie.ColorBlack)).Infof("Starting server on [%s]", host)
|
||||||
|
log.Fatal(http.ListenAndServe(":8008", 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