Skip to content

Commit

Permalink
refactor controllers and use standard library
Browse files Browse the repository at this point in the history
  • Loading branch information
sentriz committed Sep 30, 2023
1 parent adceff1 commit a9eda5b
Show file tree
Hide file tree
Showing 25 changed files with 872 additions and 912 deletions.
58 changes: 30 additions & 28 deletions cmd/gonic/gonic.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ import (
"strings"
"time"

// avatar encode/decode
_ "image/gif"
_ "image/png"

"github.com/google/shlex"
"github.com/gorilla/mux"
"github.com/gorilla/securecookie"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/oklog/run"
Expand All @@ -25,6 +28,7 @@ import (

"go.senan.xyz/gonic"
"go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/handlerutil"
"go.senan.xyz/gonic/jukebox"
"go.senan.xyz/gonic/lastfm"
"go.senan.xyz/gonic/listenbrainz"
Expand All @@ -34,7 +38,6 @@ import (
"go.senan.xyz/gonic/scanner/tags"
"go.senan.xyz/gonic/scrobble"
"go.senan.xyz/gonic/server/ctrladmin"
"go.senan.xyz/gonic/server/ctrlbase"
"go.senan.xyz/gonic/server/ctrlsubsonic"
"go.senan.xyz/gonic/server/ctrlsubsonic/artistinfocache"
"go.senan.xyz/gonic/transcode"
Expand Down Expand Up @@ -166,7 +169,7 @@ func main() {

tagger := &tags.TagReader{}
scannr := scanner.New(
ctrlsubsonic.PathsOf(musicPaths),
ctrlsubsonic.MusicPaths(musicPaths),
dbc,
map[scanner.Tag]scanner.MultiValueSetting{
scanner.Genre: scanner.MultiValueSetting(confMultiValueGenre),
Expand Down Expand Up @@ -218,37 +221,36 @@ func main() {

artistInfoCache := artistinfocache.New(dbc, lastfmClient)

ctrlBase := &ctrlbase.Controller{
DB: dbc,
PlaylistStore: playlistStore,
ProxyPrefix: *confProxyPrefix,
Scanner: scannr,
scrobblers := []scrobble.Scrobbler{lastfmClient, listenbrainzClient}

resolveProxyPath := func(in string) string {
return path.Join(*confProxyPrefix, in)
}
ctrlAdmin, err := ctrladmin.New(ctrlBase, sessDB, podcast, lastfmClient)

ctrlAdmin, err := ctrladmin.New(dbc, sessDB, scannr, podcast, lastfmClient, resolveProxyPath)
if err != nil {
log.Panicf("error creating admin controller: %v\n", err)
}
ctrlSubsonic := &ctrlsubsonic.Controller{
Controller: ctrlBase,
MusicPaths: musicPaths,
PodcastsPath: *confPodcastPath,
CacheAudioPath: cacheDirAudio,
CacheCoverPath: cacheDirCovers,
LastFMClient: lastfmClient,
ArtistInfoCache: artistInfoCache,
Scrobblers: []scrobble.Scrobbler{
lastfmClient,
listenbrainzClient,
},
Podcasts: podcast,
Transcoder: transcoder,
Jukebox: jukebx,
ctrlSubsonic, err := ctrlsubsonic.New(dbc, scannr, musicPaths, *confPodcastPath, cacheDirAudio, cacheDirCovers, jukebx, playlistStore, scrobblers, podcast, transcoder, lastfmClient, artistInfoCache, resolveProxyPath)
if err != nil {
log.Panicf("error creating subsonic controller: %v\n", err)
}

chain := handlerutil.Chain()
if *confHTTPLog {
chain = handlerutil.Chain(handlerutil.Log)
}
chain = handlerutil.Chain(
chain,
handlerutil.BasicCORS,
)
trim := handlerutil.TrimPathSuffix(".view") // /x.view and /x should match the same

mux := mux.NewRouter()
ctrlbase.AddRoutes(ctrlBase, mux, *confHTTPLog)
ctrladmin.AddRoutes(ctrlAdmin, mux.PathPrefix("/admin").Subrouter())
ctrlsubsonic.AddRoutes(ctrlSubsonic, mux.PathPrefix("/rest").Subrouter())
mux := http.NewServeMux()
mux.Handle("/admin/", http.StripPrefix("/admin", chain(ctrlAdmin)))
mux.Handle("/rest/", http.StripPrefix("/rest", chain(trim(ctrlSubsonic))))
mux.Handle("/ping", chain(handlerutil.Message("ok")))
mux.Handle("/", chain(handlerutil.Redirect(resolveProxyPath("/admin/home"))))

if *confExpvar {
mux.Handle("/debug/vars", expvar.Handler())
Expand Down
5 changes: 1 addition & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,13 @@ go 1.21
require (
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/andybalholm/cascadia v1.3.2
github.com/davecgh/go-spew v1.1.1
github.com/dexterlb/mpvipc v0.0.0-20230829142118-145d6eabdc37
github.com/disintegration/imaging v1.6.2
github.com/dustin/go-humanize v1.0.1
github.com/fatih/structs v1.1.0
github.com/fsnotify/fsnotify v1.6.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.3.1
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.1
github.com/jinzhu/gorm v1.9.17-0.20211120011537-5c235b72a414
Expand All @@ -41,7 +38,7 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/PuerkitoBio/goquery v1.8.1 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/swag v0.21.1 // indirect
github.com/gorilla/context v1.1.1 // indirect
Expand Down
7 changes: 0 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DP
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
Expand All @@ -55,10 +52,6 @@ github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
Expand Down
168 changes: 88 additions & 80 deletions server/ctrlbase/ctrl.go → handlerutil/handlerutil.go
Original file line number Diff line number Diff line change
@@ -1,100 +1,41 @@
package ctrlbase
package handlerutil

import (
"fmt"
"log"
"net/http"
"path"
"strings"

"go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/playlist"
"go.senan.xyz/gonic/scanner"
)

type statusWriter struct {
http.ResponseWriter
status int
}

func (w *statusWriter) WriteHeader(status int) {
w.status = status
w.ResponseWriter.WriteHeader(status)
}

func (w *statusWriter) Write(b []byte) (int, error) {
if w.status == 0 {
w.status = 200
}
return w.ResponseWriter.Write(b)
}
type Middleware func(http.Handler) http.Handler

func statusToBlock(code int) string {
var bg int
switch {
case 200 <= code && code <= 299:
bg = 42 // bright green, ok
case 300 <= code && code <= 399:
bg = 46 // bright cyan, redirect
case 400 <= code && code <= 499:
bg = 43 // bright orange, client error
case 500 <= code && code <= 599:
bg = 41 // bright red, server error
default:
bg = 47 // bright white (grey)
func Chain(middlewares ...Middleware) Middleware {
return func(final http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
final = middlewares[i](final)
}
return final
}
return fmt.Sprintf("\u001b[%d;1m %d \u001b[0m", bg, code)
}

type Controller struct {
DB *db.DB
PlaylistStore *playlist.Store
Scanner *scanner.Scanner
ProxyPrefix string
}

// Path returns a URL path with the proxy prefix included
func (c *Controller) Path(rel string) string {
return path.Join(c.ProxyPrefix, rel)
}

func (c *Controller) BaseURL(r *http.Request) string {
protocol := "http"
if r.TLS != nil {
protocol = "https"
func TrimPathSuffix(suffix string) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = strings.TrimSuffix(r.URL.Path, suffix)
next.ServeHTTP(w, r)
})
}
scheme := firstExisting(
protocol, // fallback
r.Header.Get("X-Forwarded-Proto"),
r.Header.Get("X-Forwarded-Scheme"),
r.URL.Scheme,
)
host := firstExisting(
"localhost:4747", // fallback
r.Header.Get("X-Forwarded-Host"),
r.Host,
)
return fmt.Sprintf("%s://%s", scheme, host)
}

func (c *Controller) WithLogging(next http.Handler) http.Handler {
func Log(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// this is (should be) the first middleware. pass right though it
// by calling `next` first instead of last. when it completes all
// other middlewares and the custom ResponseWriter has been written
sw := &statusWriter{ResponseWriter: w}
next.ServeHTTP(sw, r)

// sanitise password
if q := r.URL.Query(); q.Get("p") != "" {
q.Set("p", "REDACTED")
r.URL.RawQuery = q.Encode()
}
log.Printf("response %s for `%v`", statusToBlock(sw.status), r.URL)
log.Printf("response %s %s %v", statusToBlock(sw.status), r.Method, r.URL)
})
}

func (c *Controller) WithCORS(next http.Handler) http.Handler {
func BasicCORS(next http.Handler) http.Handler {
allowMethods := strings.Join(
[]string{http.MethodPost, http.MethodGet, http.MethodOptions, http.MethodPut, http.MethodDelete},
", ",
Expand All @@ -115,11 +56,78 @@ func (c *Controller) WithCORS(next http.Handler) http.Handler {
})
}

func firstExisting(or string, strings ...string) string {
for _, s := range strings {
if s != "" {
func Redirect(to string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, to, http.StatusSeeOther)
})
}

func Message(message string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, message)
})
}

func BaseURL(r *http.Request) string {
fallbackProtocoll := "http"
if r.TLS != nil {
fallbackProtocoll = "https"
}
fallbackHost := "localhost:4747"
scheme := first(
r.Header.Get("X-Forwarded-Proto"),
r.Header.Get("X-Forwarded-Scheme"),
r.URL.Scheme,
fallbackProtocoll,
)
host := first(
r.Header.Get("X-Forwarded-Host"),
r.Host,
fallbackHost,
)
return fmt.Sprintf("%s://%s", scheme, host)
}

type statusWriter struct {
http.ResponseWriter
status int
}

func (w *statusWriter) WriteHeader(status int) {
w.status = status
w.ResponseWriter.WriteHeader(status)
}

func (w *statusWriter) Write(b []byte) (int, error) {
if w.status == 0 {
w.status = 200
}
return w.ResponseWriter.Write(b)
}

func statusToBlock(code int) string {
var bg int
switch {
case code >= 500:
bg = 41 // bright red
case code >= 400:
bg = 43 // bright orange
case code >= 300:
bg = 46 // bright cyan
case code >= 200:
bg = 42 // bright green
default:
bg = 47 // bright white (grey)
}
return fmt.Sprintf("\u001b[%d;1m %d \u001b[0m", bg, code)
}

func first[T comparable](vs ...T) T {
var z T
for _, s := range vs {
if s != z {
return s
}
}
return or
return z
}
Loading

0 comments on commit a9eda5b

Please sign in to comment.