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}