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