willow.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: Apache-2.0
  4
  5package main
  6
  7import (
  8	"errors"
  9	"fmt"
 10	"log"
 11	"net/http"
 12	"os"
 13	"strconv"
 14
 15	"git.sr.ht/~amolith/willow/db"
 16	"git.sr.ht/~amolith/willow/project"
 17	"git.sr.ht/~amolith/willow/ws"
 18
 19	"github.com/BurntSushi/toml"
 20	flag "github.com/spf13/pflag"
 21)
 22
 23type (
 24	Config struct {
 25		Server      server
 26		CSVLocation string
 27		DBConn      string
 28		// TODO: Make cache location configurable
 29		// CacheLocation string
 30		FetchInterval int
 31	}
 32
 33	server struct {
 34		Listen string
 35	}
 36)
 37
 38var (
 39	flagConfig          = flag.StringP("config", "c", "config.toml", "Path to config file")
 40	flagAddUser         = flag.StringP("add", "a", "", "Username of account to add")
 41	flagDeleteUser      = flag.StringP("deleteuser", "d", "", "Username of account to delete")
 42	flagCheckAuthorised = flag.StringP("validatecredentials", "v", "", "Username of account to check")
 43	flagListUsers       = flag.BoolP("listusers", "l", false, "List all users")
 44	config              Config
 45	req                 = make(chan struct{})
 46	res                 = make(chan []project.Project)
 47	manualRefresh       = make(chan struct{})
 48)
 49
 50func main() {
 51	flag.Parse()
 52
 53	err := checkConfig()
 54	if err != nil {
 55		log.Fatalln(err)
 56	}
 57
 58	fmt.Println("Opening database at", config.DBConn)
 59
 60	dbConn, err := db.Open(config.DBConn)
 61	if err != nil {
 62		fmt.Println("Error opening database:", err)
 63		os.Exit(1)
 64	}
 65
 66	fmt.Println("Checking whether database needs initialising")
 67	err = db.InitialiseDatabase(dbConn)
 68	if err != nil {
 69		fmt.Println("Error initialising database:", err)
 70		os.Exit(1)
 71	}
 72	fmt.Println("Checking whether there are pending migrations")
 73	err = db.Migrate(dbConn)
 74	if err != nil {
 75		fmt.Println("Error migrating database schema:", err)
 76		os.Exit(1)
 77	}
 78
 79	if len(*flagAddUser) > 0 && len(*flagDeleteUser) == 0 && !*flagListUsers && len(*flagCheckAuthorised) == 0 {
 80		createUser(dbConn, *flagAddUser)
 81		os.Exit(0)
 82	} else if len(*flagAddUser) == 0 && len(*flagDeleteUser) > 0 && !*flagListUsers && len(*flagCheckAuthorised) == 0 {
 83		deleteUser(dbConn, *flagDeleteUser)
 84		os.Exit(0)
 85	} else if len(*flagAddUser) == 0 && len(*flagDeleteUser) == 0 && *flagListUsers && len(*flagCheckAuthorised) == 0 {
 86		listUsers(dbConn)
 87		os.Exit(0)
 88	} else if len(*flagAddUser) == 0 && len(*flagDeleteUser) == 0 && !*flagListUsers && len(*flagCheckAuthorised) > 0 {
 89		checkAuthorised(dbConn, *flagCheckAuthorised)
 90		os.Exit(0)
 91	}
 92
 93	fmt.Println("Starting refresh loop")
 94	go project.RefreshLoop(dbConn, config.FetchInterval, &manualRefresh, &req, &res)
 95
 96	var mutex sync.Mutex
 97
 98	wsHandler := ws.Handler{
 99		DbConn:        dbConn,
100		Mutex:         &mutex,
101		Req:           &req,
102		Res:           &res,
103		ManualRefresh: &manualRefresh,
104	}
105
106	mux := http.NewServeMux()
107	mux.HandleFunc("/static/", ws.StaticHandler)
108	mux.HandleFunc("/new", wsHandler.NewHandler)
109	mux.HandleFunc("/login", wsHandler.LoginHandler)
110	mux.HandleFunc("/logout", wsHandler.LogoutHandler)
111	mux.HandleFunc("/", wsHandler.RootHandler)
112
113	httpServer := &http.Server{
114		Addr:    config.Server.Listen,
115		Handler: mux,
116	}
117
118	fmt.Println("Starting web server on", config.Server.Listen)
119	if err := httpServer.ListenAndServe(); errors.Is(err, http.ErrServerClosed) {
120		fmt.Println("Web server closed")
121		os.Exit(0)
122	} else {
123		fmt.Println(err)
124		os.Exit(1)
125	}
126}
127
128func checkConfig() error {
129	defaultDBConn := "willow.sqlite"
130	defaultFetchInterval := 3600
131	defaultListen := "127.0.0.1:1313"
132
133	defaultConfig := fmt.Sprintf(`# Path to SQLite database
134DBConn = "%s"
135# How often to fetch new releases in seconds
136## Minimum is %ds to avoid rate limits and unintentional abuse
137FetchInterval = %d
138
139[Server]
140# Address to listen on
141Listen = "%s"`, defaultDBConn, defaultFetchInterval, defaultFetchInterval, defaultListen)
142
143	file, err := os.Open(*flagConfig)
144	if err != nil {
145		if os.IsNotExist(err) {
146			file, err = os.Create(*flagConfig)
147			if err != nil {
148				return err
149			}
150			defer file.Close()
151
152			_, err = file.WriteString(defaultConfig)
153			if err != nil {
154				return err
155			}
156
157			fmt.Println("Config file created at", *flagConfig)
158			fmt.Println("Please edit it and restart the server")
159			os.Exit(0)
160		} else {
161			return err
162		}
163	}
164	defer file.Close()
165
166	_, err = toml.DecodeFile(*flagConfig, &config)
167	if err != nil {
168		return err
169	}
170
171	if config.FetchInterval < defaultFetchInterval {
172		fmt.Println("Fetch interval is set to", strconv.Itoa(config.FetchInterval), "seconds, but the minimum is", defaultFetchInterval, "seconds, using", strconv.Itoa(defaultFetchInterval)+"s")
173		config.FetchInterval = defaultFetchInterval
174	}
175
176	if config.Server.Listen == "" {
177		fmt.Println("No listen address specified, using", defaultListen)
178		config.Server.Listen = defaultListen
179	}
180
181	if config.DBConn == "" {
182		fmt.Println("No SQLite path specified, using \"" + defaultDBConn + "\"")
183		config.DBConn = defaultDBConn
184	}
185
186	return nil
187}