start adding method handlers
ci/woodpecker/push/woodpecker Pipeline was successful Details

This commit is contained in:
David 2024-01-01 17:31:18 -05:00
parent 034232f866
commit fa73364107
3 changed files with 195 additions and 153 deletions

View File

@ -2,13 +2,12 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "io"
"io/fs" "io/fs"
"net/http" "net/http"
"git.yetaga.in/alazyreader/library/media" "git.yetaga.in/alazyreader/library/media"
"tailscale.com/client/tailscale" "tailscale.com/client/tailscale"
"tailscale.com/util/must"
) )
type Router struct { type Router struct {
@ -23,98 +22,175 @@ type AdminRouter struct {
ts *tailscale.LocalClient ts *tailscale.LocalClient
} }
func writeError[T any](w http.ResponseWriter) func(t T, err error) (T, bool) { type handler struct {
return func(t T, err error) (T, bool) { get func()
post func()
put func()
delete func()
}
func (h handler) Handle(w http.ResponseWriter, req *http.Request) {
if req.Method == http.MethodHead && h.get != nil {
h.get()
} else if req.Method == http.MethodGet && h.get != nil {
h.get()
} else if req.Method == http.MethodPost && h.post != nil {
h.post()
} else if req.Method == http.MethodPut && h.put != nil {
h.put()
} else if req.Method == http.MethodDelete && h.delete != nil {
h.delete()
} else {
badMethod(w)
}
}
func writeError[T any](t T, err error) func(w http.ResponseWriter) (T, bool) {
return func(w http.ResponseWriter) (T, bool) {
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
} }
return t, err != nil return t, err == nil
} }
} }
func writeJSON(w http.ResponseWriter, b []byte, status int) { func writeNoBody(w http.ResponseWriter, status int) {
w.WriteHeader(status)
}
func writeJSON(w http.ResponseWriter, b any, status int) {
bytes, err := json.Marshal(b)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status) w.WriteHeader(status)
w.Write(b) w.Write(bytes)
w.Write([]byte("\n")) w.Write([]byte("\n"))
} }
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if req.URL.Path == "/api/records" { switch r.URL.Path {
RecordsAPIHandler(r.rcol).ServeHTTP(w, req) case "/api/records":
return handler{
get: func() { getRecords(router.rcol, w, r) },
}.Handle(w, r)
case "/api/books":
handler{
get: func() { getBooks(router.lib, w, r) },
}.Handle(w, r)
default:
static(router.static).ServeHTTP(w, r)
} }
if req.URL.Path == "/api/books" {
BooksAPIHandler(r.lib).ServeHTTP(w, req)
return
}
StaticHandler(r.static).ServeHTTP(w, req)
} }
func (r *AdminRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (router *AdminRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
whois, _ := r.ts.WhoIs(req.Context(), req.RemoteAddr) switch r.URL.Path {
switch req.URL.Path { case "/api/whoami":
handler{
get: func() { getWhoAmI(router.ts, w, r) },
}.Handle(w, r)
case "/api/books": case "/api/books":
switch req.Method { handler{
case http.MethodGet: get: func() { getBooks(router.lib, w, r) },
books, ok := writeError[[]media.Book](w)(r.lib.GetAllBooks(req.Context())) post: func() { addBook(router.lib, w, r) },
if !ok { delete: func() { deleteBook(router.lib, w, r) },
return }.Handle(w, r)
} default:
b, ok := writeError[[]byte](w)(json.Marshal(books)) static(router.static).ServeHTTP(w, r)
if !ok {
return
}
writeJSON(w, b, http.StatusOK)
case http.MethodPost:
default:
badMethod(w)
}
return
} }
w.Write([]byte(fmt.Sprintf("%+v", whois.UserProfile.DisplayName)))
// StaticHandler(r.static).ServeHTTP(w, req)
} }
func badMethod(w http.ResponseWriter) { func badMethod(w http.ResponseWriter) {
writeJSON(w, writeJSON(w,
must.Get(json.Marshal(struct{ Error string }{Error: "method not supported"})), struct{ Error string }{Error: "method not supported"},
http.StatusMethodNotAllowed) http.StatusMethodNotAllowed)
} }
func BooksAPIHandler(l Library) http.Handler { func getBooks(l Library, w http.ResponseWriter, r *http.Request) {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { books, err := l.GetAllBooks(r.Context())
books, err := l.GetAllBooks(r.Context()) if err != nil {
if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusInternalServerError) return
return }
} writeJSON(w, books, http.StatusOK)
b, err := json.Marshal(books)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, b, http.StatusOK)
})
} }
func RecordsAPIHandler(l RecordCollection) http.Handler { func addBook(l Library, w http.ResponseWriter, r *http.Request) {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Body == nil {
books, err := l.GetAllRecords(r.Context()) http.Error(w, "no body provided", http.StatusBadRequest)
if err != nil { return
http.Error(w, err.Error(), http.StatusInternalServerError) }
return defer r.Body.Close()
} b, err := io.ReadAll(r.Body)
b, err := json.Marshal(books) if err != nil {
if err != nil { http.Error(w, "error reading body", http.StatusBadRequest)
http.Error(w, err.Error(), http.StatusInternalServerError) return
return }
} book := &media.Book{}
writeJSON(w, b, http.StatusOK) err = json.Unmarshal(b, book)
}) if err != nil {
http.Error(w, "error parsing body", http.StatusBadRequest)
return
}
err = l.AddBook(r.Context(), book)
if err != nil {
http.Error(w, "error parsing body", http.StatusBadRequest)
return
}
writeNoBody(w, http.StatusAccepted)
} }
func StaticHandler(f fs.FS) http.Handler { func deleteBook(l Library, w http.ResponseWriter, r *http.Request) {
if r.Body == nil {
http.Error(w, "no body provided", http.StatusBadRequest)
return
}
defer r.Body.Close()
b, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "error reading body", http.StatusBadRequest)
return
}
book := &media.Book{}
err = json.Unmarshal(b, book)
if err != nil {
http.Error(w, "error parsing body", http.StatusBadRequest)
return
}
err = l.DeleteBook(r.Context(), book)
if err != nil {
http.Error(w, "error parsing body", http.StatusBadRequest)
return
}
writeNoBody(w, http.StatusAccepted)
}
func getRecords(l RecordCollection, w http.ResponseWriter, r *http.Request) {
records, err := l.GetAllRecords(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, records, http.StatusOK)
}
func getWhoAmI(ts *tailscale.LocalClient, w http.ResponseWriter, r *http.Request) {
whois, ok := writeError(ts.WhoIs(r.Context(), r.RemoteAddr))(w)
if !ok {
return
}
writeJSON(w, struct {
Username string `json:"Username"`
DisplayName string `json:"DisplayName"`
ProfilePicURL string `json:"ProfilePicURL"`
}{
Username: whois.UserProfile.LoginName,
DisplayName: whois.UserProfile.DisplayName,
ProfilePicURL: whois.UserProfile.ProfilePicURL,
}, http.StatusOK)
}
func static(f fs.FS) http.Handler {
return http.FileServer(http.FS(f)) return http.FileServer(http.FS(f))
} }

View File

@ -27,6 +27,8 @@ func obscureStr(in string, l int) string {
type Library interface { type Library interface {
GetAllBooks(context.Context) ([]media.Book, error) GetAllBooks(context.Context) ([]media.Book, error)
AddBook(context.Context, *media.Book) error
DeleteBook(context.Context, *media.Book) error
} }
type RecordCollection interface { type RecordCollection interface {
@ -56,20 +58,26 @@ func main() {
frontendRoot := must.Get(frontend.Root()) frontendRoot := must.Get(frontend.Root())
adminRoot := must.Get(frontend.AdminRoot()) adminRoot := must.Get(frontend.AdminRoot())
servers := make(chan (*http.Server), 2) servers := make(chan (*http.Server), 3)
errGroup := errgroup.Group{} errGroup := errgroup.Group{}
errGroup.Go(start(servers)( errGroup.Go(func() error {
publicServer(8080, &Router{ return start(servers)(
static: frontendRoot, publicServer(8080, &Router{
lib: lib, static: frontendRoot,
rcol: discogsCache, lib: lib,
}))) rcol: discogsCache,
errGroup.Go(start(servers)( }))
tailscaleListener("library-admin", &AdminRouter{ })
static: adminRoot, errGroup.Go(func() error {
lib: lib, return start(servers)(
}))) tailscaleListener("library-admin", &AdminRouter{
errGroup.Go(shutdown(servers)) static: adminRoot,
lib: lib,
}))
})
errGroup.Go(func() error {
return shutdown(servers)
})
log.Println(errGroup.Wait()) log.Println(errGroup.Wait())
} }
@ -99,33 +107,30 @@ func setupSQL(c config.Config) (Library, int, int, error) {
return sql, latest, run, nil return sql, latest, run, nil
} }
func errFunc(err error) func() error { func start(servers chan (*http.Server)) func(*http.Server, net.Listener, error) error {
return func() error { return err } return func(s *http.Server, l net.Listener, err error) error {
}
func start(servers chan (*http.Server)) func(*http.Server, net.Listener, error) func() error {
return func(s *http.Server, l net.Listener, err error) func() error {
if err != nil { if err != nil {
return errFunc(err) return err
} }
servers <- s servers <- s
return errFunc(s.Serve(l)) return s.Serve(l)
} }
} }
func shutdown(servers chan (*http.Server)) func() error { func shutdown(servers chan (*http.Server)) error {
sigint := make(chan os.Signal, 1) sigint := make(chan os.Signal, 1)
signal.Notify(sigint, os.Interrupt) signal.Notify(sigint, os.Interrupt)
<-sigint <-sigint
close(servers) close(servers)
var err error
for server := range servers { for server := range servers {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
if err := server.Shutdown(ctx); err != nil { if shutdownerr := server.Shutdown(ctx); shutdownerr != nil {
log.Panicf("error during shutdown: %v", err) err = shutdownerr
} }
cancel() cancel()
} }
return errFunc(nil) return err
} }
func publicServer(port int, handler http.Handler) (*http.Server, net.Listener, error) { func publicServer(port int, handler http.Handler) (*http.Server, net.Listener, error) {
@ -134,6 +139,7 @@ func publicServer(port int, handler http.Handler) (*http.Server, net.Listener, e
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
log.Println("starting public server")
return server, ln, nil return server, ln, nil
} }

View File

@ -153,9 +153,8 @@ func (m *MySQL) GetAllBooks(ctx context.Context) ([]media.Book, error) {
} }
allBooksQuery := fmt.Sprintf(`SELECT allBooksQuery := fmt.Sprintf(`SELECT
id, title, authors, sortauthor, isbn10, isbn13, format, id, title, authors, sortauthor, isbn10, isbn13, format, genre, publisher,
genre, publisher, series, volume, year, signed, series, volume, year, signed, description, notes, coverurl, childrens
description, notes, coverurl, childrens
FROM %s`, m.tableName) FROM %s`, m.tableName)
books := []media.Book{} books := []media.Book{}
@ -169,12 +168,8 @@ func (m *MySQL) GetAllBooks(ctx context.Context) ([]media.Book, error) {
b := media.Book{} b := media.Book{}
var authors string var authors string
err := rows.Scan( err := rows.Scan(
&b.ID, &b.Title, &authors, &b.ID, &b.Title, &authors, &b.SortAuthor, &b.ISBN10, &b.ISBN13, &b.Format, &b.Genre, &b.Publisher,
&b.SortAuthor, &b.ISBN10, &b.ISBN13, &b.Series, &b.Volume, &b.Year, &b.Signed, &b.Description, &b.Notes, &b.CoverURL, &b.Childrens)
&b.Format, &b.Genre, &b.Publisher,
&b.Series, &b.Volume, &b.Year,
&b.Signed, &b.Description, &b.Notes,
&b.CoverURL, &b.Childrens)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -193,25 +188,14 @@ func (m *MySQL) AddBook(ctx context.Context, b *media.Book) error {
res, err := m.connection.ExecContext(ctx, ` res, err := m.connection.ExecContext(ctx, `
INSERT INTO `+m.tableName+` INSERT INTO `+m.tableName+`
(title, authors, sortauthor, isbn10, isbn13, format, genre, publisher, series, volume, year, signed, description, notes, coverurl, childrens) (
title, authors, sortauthor, isbn10, isbn13, format, genre, publisher, series,
volume, year, signed, description, notes, coverurl, childrens
)
VALUES VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
b.Title, b.Title, strings.Join(b.Authors, ";"), b.SortAuthor, b.ISBN10, b.ISBN13, b.Format, b.Genre, b.Publisher, b.Series,
strings.Join(b.Authors, ";"), b.Volume, b.Year, b.Signed, b.Description, b.Notes, b.CoverURL, b.Childrens,
b.SortAuthor,
b.ISBN10,
b.ISBN13,
b.Format,
b.Genre,
b.Publisher,
b.Series,
b.Volume,
b.Year,
b.Signed,
b.Description,
b.Notes,
b.CoverURL,
b.Childrens,
) )
if err != nil { if err != nil {
return err return err
@ -236,41 +220,13 @@ func (m *MySQL) UpdateBook(ctx context.Context, old, new *media.Book) error {
res, err := m.connection.ExecContext(ctx, ` res, err := m.connection.ExecContext(ctx, `
UPDATE `+m.tableName+` UPDATE `+m.tableName+`
SET id=? SET
title=? id=? title=? authors=? sortauthor=? isbn10=? isbn13=? format=? genre=? publisher=?
authors=? series=? volume=? year=? signed=? description=? notes=? coverurl=? childrens=?
sortauthor=?
isbn10=?
isbn13=?
format=?
genre=?
publisher=?
series=?
volume=?
year=?
signed=?
description=?
notes=?
coverurl=?
childrens=?
WHERE id=?`, WHERE id=?`,
new.Title, new.Title, strings.Join(new.Authors, ";"), new.SortAuthor, new.ISBN10, new.ISBN13, new.Format, new.Genre, new.Publisher,
strings.Join(new.Authors, ";"), new.Series, new.Volume, new.Year, new.Signed, new.Description, new.Notes, new.CoverURL, new.Childrens, old.ID,
new.SortAuthor, )
new.ISBN10,
new.ISBN13,
new.Format,
new.Genre,
new.Publisher,
new.Series,
new.Volume,
new.Year,
new.Signed,
new.Description,
new.Notes,
new.CoverURL,
new.Childrens,
old.ID)
if err != nil { if err != nil {
return err return err
} }
@ -284,6 +240,10 @@ func (m *MySQL) UpdateBook(ctx context.Context, old, new *media.Book) error {
return nil return nil
} }
func (m *MySQL) DeleteBook(_ context.Context, b *media.Book) error {
return nil
}
func parseMigrationFileName(filename string) (int, string, error) { func parseMigrationFileName(filename string) (int, string, error) {
sp := strings.SplitN(filename, "-", 2) sp := strings.SplitN(filename, "-", 2)
i, err := strconv.Atoi(sp[0]) i, err := strconv.Atoi(sp[0])