All checks were successful
		
		
	
	ci/woodpecker/push/woodpecker Pipeline was successful
				
			Reviewed-on: #17 Co-authored-by: David Ashby <delta.mu.alpha@gmail.com> Co-committed-by: David Ashby <delta.mu.alpha@gmail.com>
		
			
				
	
	
		
			171 lines
		
	
	
		
			4.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			171 lines
		
	
	
		
			4.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package main
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"log"
 | |
| 	"net"
 | |
| 	"net/http"
 | |
| 	"os"
 | |
| 	"os/signal"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"git.yetaga.in/alazyreader/library/config"
 | |
| 	"git.yetaga.in/alazyreader/library/database"
 | |
| 	"git.yetaga.in/alazyreader/library/frontend"
 | |
| 	"git.yetaga.in/alazyreader/library/media"
 | |
| 	"git.yetaga.in/alazyreader/library/query"
 | |
| 	"github.com/kelseyhightower/envconfig"
 | |
| 	"golang.org/x/sync/errgroup"
 | |
| 	"tailscale.com/tsnet"
 | |
| 	"tailscale.com/util/must"
 | |
| )
 | |
| 
 | |
| func obscureStr(in string, l int) string {
 | |
| 	return in[0:max(l, len(in))] + strings.Repeat("*", max(0, len(in)-l))
 | |
| }
 | |
| 
 | |
| type Library interface {
 | |
| 	GetAllBooks(context.Context) ([]media.Book, error)
 | |
| 	AddBook(context.Context, *media.Book) error
 | |
| 	DeleteBook(context.Context, *media.Book) error
 | |
| }
 | |
| 
 | |
| type RecordCollection interface {
 | |
| 	GetAllRecords(context.Context) ([]media.Record, error)
 | |
| }
 | |
| 
 | |
| type Query interface {
 | |
| 	GetByISBN(string) (*media.Book, error)
 | |
| }
 | |
| 
 | |
| func main() {
 | |
| 	var c config.Config
 | |
| 	must.Do(envconfig.Process("library", &c))
 | |
| 
 | |
| 	var lib Library
 | |
| 	if c.DBType == "memory" {
 | |
| 		lib = &database.Memory{}
 | |
| 	} else if c.DBType == "sql" {
 | |
| 		sqllib, latest, run, err := setupSQL(c)
 | |
| 		if err != nil {
 | |
| 			log.Fatalf("sql connection err: %v", err)
 | |
| 		}
 | |
| 		log.Printf("latest migration: %d; migrations run: %d", latest, run)
 | |
| 		lib = sqllib
 | |
| 	}
 | |
| 	discogsCache := must.Get(database.NewDiscogsCache(
 | |
| 		c.DiscogsToken, time.Hour*24, c.DiscogsUser, c.DiscogsPersist, c.DiscogsCacheFile,
 | |
| 	))
 | |
| 	queryProvider := &query.GoogleBooks{}
 | |
| 	staticRoot := must.Get(frontend.Root())
 | |
| 
 | |
| 	servers := make(chan (*http.Server), 3)
 | |
| 	errGroup := errgroup.Group{}
 | |
| 	errGroup.Go(func() error {
 | |
| 		return start(servers)(publicServer(8080, &Router{
 | |
| 			static:  staticRoot,
 | |
| 			lib:     lib,
 | |
| 			rcol:    discogsCache,
 | |
| 			isAdmin: false,
 | |
| 		}))
 | |
| 	})
 | |
| 	errGroup.Go(func() error {
 | |
| 		return start(servers)(tailscaleListener("library-admin", &Router{
 | |
| 			static:  staticRoot,
 | |
| 			lib:     lib,
 | |
| 			rcol:    discogsCache,
 | |
| 			query:   queryProvider,
 | |
| 			isAdmin: true,
 | |
| 		}))
 | |
| 	})
 | |
| 	errGroup.Go(func() error {
 | |
| 		return shutdown(servers)
 | |
| 	})
 | |
| 	log.Println(errGroup.Wait())
 | |
| }
 | |
| 
 | |
| func setupSQL(c config.Config) (Library, int, int, error) {
 | |
| 	if c.DBUser == "" || c.DBPass == "" || c.DBHost == "" || c.DBPort == "" || c.DBName == "" {
 | |
| 		if c.DBPass != "" {
 | |
| 			c.DBPass = obscureStr(c.DBPass, 3)
 | |
| 		}
 | |
| 		if c.DiscogsToken != "" {
 | |
| 			c.DiscogsToken = obscureStr(c.DiscogsToken, 3)
 | |
| 		}
 | |
| 		return nil, 0, 0, fmt.Errorf("invalid config; vars provided: %+v", c)
 | |
| 	}
 | |
| 	sql, err := database.NewMySQLConnection(c.DBUser, c.DBPass, c.DBHost, c.DBPort, c.DBName)
 | |
| 	if err != nil {
 | |
| 		return nil, 0, 0, err
 | |
| 	}
 | |
| 	err = sql.PrepareDatabase(context.Background())
 | |
| 	if err != nil {
 | |
| 		return nil, 0, 0, err
 | |
| 	}
 | |
| 	latest, run, err := sql.RunMigrations(context.Background())
 | |
| 	if err != nil {
 | |
| 		return nil, 0, 0, err
 | |
| 	}
 | |
| 	return sql, latest, run, nil
 | |
| }
 | |
| 
 | |
| 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 err
 | |
| 		}
 | |
| 		servers <- s
 | |
| 		return s.Serve(l)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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 shutdownerr := server.Shutdown(ctx); shutdownerr != nil {
 | |
| 			err = shutdownerr
 | |
| 		}
 | |
| 		cancel()
 | |
| 	}
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| func publicServer(port int, handler http.Handler) (*http.Server, net.Listener, error) {
 | |
| 	server := &http.Server{Handler: handler}
 | |
| 	ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
 | |
| 	if err != nil {
 | |
| 		return nil, nil, err
 | |
| 	}
 | |
| 	log.Printf("public server: http://0.0.0.0:%d/", port)
 | |
| 	return server, ln, nil
 | |
| }
 | |
| 
 | |
| func tailscaleListener(hostname string, handler *Router) (*http.Server, net.Listener, error) {
 | |
| 	s := &tsnet.Server{
 | |
| 		Dir:      ".config/" + hostname,
 | |
| 		Hostname: hostname,
 | |
| 		Logf: func(s string, a ...any) { // silence most tsnet logs
 | |
| 			if strings.HasPrefix(s, "To start this tsnet server") {
 | |
| 				log.Printf(s, a...)
 | |
| 			}
 | |
| 		},
 | |
| 	}
 | |
| 	ln, err := s.Listen("tcp", ":80")
 | |
| 	if err != nil {
 | |
| 		return nil, nil, err
 | |
| 	}
 | |
| 	handler.ts, err = s.LocalClient()
 | |
| 	if err != nil {
 | |
| 		return nil, nil, err
 | |
| 	}
 | |
| 	log.Printf("management server: http://%s/", hostname)
 | |
| 	return &http.Server{Handler: handler}, ln, nil
 | |
| }
 |