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