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 (
"encoding/json"
"fmt"
"io"
"io/fs"
"net/http"
"git.yetaga.in/alazyreader/library/media"
"tailscale.com/client/tailscale"
"tailscale.com/util/must"
)
type Router struct {
@ -23,98 +22,175 @@ type AdminRouter struct {
ts *tailscale.LocalClient
}
func writeError[T any](w http.ResponseWriter) func(t T, err error) (T, bool) {
return func(t T, err error) (T, bool) {
type handler struct {
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 {
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.WriteHeader(status)
w.Write(b)
w.Write(bytes)
w.Write([]byte("\n"))
}
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/api/records" {
RecordsAPIHandler(r.rcol).ServeHTTP(w, req)
return
func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/records":
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) {
whois, _ := r.ts.WhoIs(req.Context(), req.RemoteAddr)
switch req.URL.Path {
func (router *AdminRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/whoami":
handler{
get: func() { getWhoAmI(router.ts, w, r) },
}.Handle(w, r)
case "/api/books":
switch req.Method {
case http.MethodGet:
books, ok := writeError[[]media.Book](w)(r.lib.GetAllBooks(req.Context()))
if !ok {
return
}
b, ok := writeError[[]byte](w)(json.Marshal(books))
if !ok {
return
}
writeJSON(w, b, http.StatusOK)
case http.MethodPost:
default:
badMethod(w)
}
return
handler{
get: func() { getBooks(router.lib, w, r) },
post: func() { addBook(router.lib, w, r) },
delete: func() { deleteBook(router.lib, w, r) },
}.Handle(w, r)
default:
static(router.static).ServeHTTP(w, r)
}
w.Write([]byte(fmt.Sprintf("%+v", whois.UserProfile.DisplayName)))
// StaticHandler(r.static).ServeHTTP(w, req)
}
func badMethod(w http.ResponseWriter) {
writeJSON(w,
must.Get(json.Marshal(struct{ Error string }{Error: "method not supported"})),
struct{ Error string }{Error: "method not supported"},
http.StatusMethodNotAllowed)
}
func BooksAPIHandler(l Library) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
books, err := l.GetAllBooks(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
b, err := json.Marshal(books)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, b, http.StatusOK)
})
func getBooks(l Library, w http.ResponseWriter, r *http.Request) {
books, err := l.GetAllBooks(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, books, http.StatusOK)
}
func RecordsAPIHandler(l RecordCollection) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
books, err := l.GetAllRecords(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
b, err := json.Marshal(books)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, b, http.StatusOK)
})
func addBook(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.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))
}

View File

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

View File

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