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 if len(*flagAddUser) > 0 && len(*flagDeleteUser) == 0 && !*flagListUsers && len(*flagCheckAuthorised) == 0 {
87 createUser(dbConn, *flagAddUser)
88 os.Exit(0)
89 } else if len(*flagAddUser) == 0 && len(*flagDeleteUser) > 0 && !*flagListUsers && len(*flagCheckAuthorised) == 0 {
90 deleteUser(dbConn, *flagDeleteUser)
91 os.Exit(0)
92 } else if len(*flagAddUser) == 0 && len(*flagDeleteUser) == 0 && *flagListUsers && len(*flagCheckAuthorised) == 0 {
93 listUsers(dbConn)
94 os.Exit(0)
95 } else if len(*flagAddUser) == 0 && len(*flagDeleteUser) == 0 && !*flagListUsers && len(*flagCheckAuthorised) > 0 {
96 checkAuthorised(dbConn, *flagCheckAuthorised)
97 os.Exit(0)
98 }
99
100 mu := sync.Mutex{}
101
102 fmt.Println("Starting refresh loop")
103 go project.RefreshLoop(dbConn, &mu, config.FetchInterval, &manualRefresh, &req, &res)
104
105 wsHandler := ws.Handler{
106 DbConn: dbConn,
107 Req: &req,
108 Res: &res,
109 ManualRefresh: &manualRefresh,
110 Mu: &mu,
111 Version: &version,
112 }
113
114 mux := http.NewServeMux()
115 mux.HandleFunc("/static/", ws.StaticHandler)
116 mux.HandleFunc("/new", wsHandler.NewHandler)
117 mux.HandleFunc("/login", wsHandler.LoginHandler)
118 mux.HandleFunc("/logout", wsHandler.LogoutHandler)
119 mux.HandleFunc("/", wsHandler.RootHandler)
120
121 httpServer := &http.Server{
122 Addr: config.Server.Listen,
123 Handler: mux,
124 }
125
126 fmt.Println("Starting web server on", config.Server.Listen)
127 if err := httpServer.ListenAndServe(); errors.Is(err, http.ErrServerClosed) {
128 fmt.Println("Web server closed")
129 os.Exit(0)
130 } else {
131 fmt.Println(err)
132 os.Exit(1)
133 }
134}
135
136func checkConfig() error {
137 defaultDBConn := "willow.sqlite"
138 defaultFetchInterval := 3600
139 defaultListen := "127.0.0.1:1313"
140
141 defaultConfig := fmt.Sprintf(`# Path to SQLite database
142DBConn = "%s"
143# How often to fetch new releases in seconds
144## Minimum is %ds to avoid rate limits and unintentional abuse
145FetchInterval = %d
146
147[Server]
148# Address to listen on
149Listen = "%s"`, defaultDBConn, defaultFetchInterval, defaultFetchInterval, defaultListen)
150
151 file, err := os.Open(*flagConfig)
152 if err != nil {
153 if os.IsNotExist(err) {
154 file, err = os.Create(*flagConfig)
155 if err != nil {
156 return err
157 }
158 defer file.Close()
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 os.Exit(0)
168 } else {
169 return err
170 }
171 }
172 defer file.Close()
173
174 _, err = toml.DecodeFile(*flagConfig, &config)
175 if err != nil {
176 return err
177 }
178
179 if config.FetchInterval < defaultFetchInterval {
180 fmt.Println("Fetch interval is set to", strconv.Itoa(config.FetchInterval), "seconds, but the minimum is", defaultFetchInterval, "seconds, using", strconv.Itoa(defaultFetchInterval)+"s")
181 config.FetchInterval = defaultFetchInterval
182 }
183
184 if config.Server.Listen == "" {
185 fmt.Println("No listen address specified, using", defaultListen)
186 config.Server.Listen = defaultListen
187 }
188
189 if config.DBConn == "" {
190 fmt.Println("No SQLite path specified, using \"" + defaultDBConn + "\"")
191 config.DBConn = defaultDBConn
192 }
193
194 return nil
195}