1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5package main
6
7import (
8 "encoding/csv"
9 "errors"
10 "fmt"
11 "github.com/BurntSushi/toml"
12 "log"
13 "net/http"
14 "os"
15 "sort"
16 "strings"
17 "time"
18
19 "github.com/microcosm-cc/bluemonday"
20 flag "github.com/spf13/pflag"
21)
22
23type (
24 Model struct {
25 Projects []project
26 }
27
28 project struct {
29 URL string
30 Name string
31 Forge string
32 Running string
33 Releases []release
34 }
35
36 release struct {
37 Tag string
38 Content string
39 URL string
40 Date time.Time
41 }
42
43 Config struct {
44 Server server
45 CSVLocation string
46 // TODO: Make cache location configurable
47 // CacheLocation string
48 FetchInterval int
49 }
50
51 server struct {
52 Listen string
53 }
54)
55
56var (
57 flagConfig *string = flag.StringP("config", "c", "config.toml", "Path to config file")
58 config Config
59 req = make(chan struct{})
60 manualRefresh = make(chan struct{})
61 res = make(chan []project)
62 m = Model{
63 Projects: []project{},
64 }
65 bmUGC = bluemonday.UGCPolicy()
66 bmStrict = bluemonday.StrictPolicy()
67)
68
69func main() {
70
71 flag.Parse()
72
73 err := checkConfig()
74 if err != nil {
75 log.Fatalln(err)
76 }
77
78 err = checkCSV()
79 if err != nil {
80 log.Fatalln(err)
81 }
82
83 reader := csv.NewReader(strings.NewReader(config.CSVLocation))
84
85 records, err := reader.ReadAll()
86 if err != nil {
87 log.Fatalln(err)
88 }
89
90 m.Projects = []project{}
91 if len(records) > 0 {
92 for i, record := range records {
93 if i == 0 {
94 continue
95 }
96 m.Projects = append(m.Projects, project{
97 URL: record[0],
98 Name: record[1],
99 Forge: record[2],
100 Running: record[3],
101 Releases: []release{},
102 })
103 }
104 }
105
106 go refreshLoop(manualRefresh, req, res)
107
108 mux := http.NewServeMux()
109
110 httpServer := &http.Server{
111 Addr: config.Server.Listen,
112 Handler: mux,
113 }
114
115 mux.HandleFunc("/", rootHandler)
116 mux.HandleFunc("/static", staticHandler)
117 mux.HandleFunc("/new", newHandler)
118
119 if err := httpServer.ListenAndServe(); errors.Is(err, http.ErrServerClosed) {
120 log.Println("Web server closed")
121 } else {
122 log.Fatalln(err)
123 }
124}
125
126func refreshLoop(manualRefresh, req chan struct{}, res chan []project) {
127 ticker := time.NewTicker(time.Second * 3600)
128
129 fetch := func() []project {
130 projects := make([]project, len(m.Projects))
131 copy(projects, m.Projects)
132 for i, project := range projects {
133 project, err := getReleases(project)
134 if err != nil {
135 fmt.Println(err)
136 continue
137 }
138 projects[i] = project
139 }
140 sort.Slice(projects, func(i, j int) bool { return strings.ToLower(projects[i].Name) < strings.ToLower(projects[j].Name) })
141 return projects
142 }
143
144 projects := fetch()
145
146 for {
147 select {
148 case <-ticker.C:
149 projects = fetch()
150 case <-manualRefresh:
151 ticker.Reset(time.Second * 3600)
152 projects = fetch()
153 case <-req:
154 projectsCopy := make([]project, len(projects))
155 copy(projectsCopy, projects)
156 res <- projectsCopy
157 }
158 }
159}
160
161func checkConfig() error {
162 file, err := os.Open(*flagConfig)
163 if err != nil {
164 if os.IsNotExist(err) {
165 file, err = os.Create(*flagConfig)
166 if err != nil {
167 return err
168 }
169 defer file.Close()
170
171 _, err = file.WriteString("# Location of the CSV file containing the projects\nCSVLocation = \"projects.csv\"\n# How often to fetch new releases in seconds\nFetchInterval = 3600\n\n[Server]\n# Address to listen on\nListen = \"127.0.0.1:1313\"\n")
172 if err != nil {
173 return err
174 }
175
176 fmt.Println("Config file created at", *flagConfig)
177 fmt.Println("Please edit it and restart the server")
178 os.Exit(0)
179 } else {
180 return err
181 }
182 }
183 defer file.Close()
184
185 _, err = toml.DecodeFile(*flagConfig, &config)
186 if err != nil {
187 return err
188 }
189
190 if config.CSVLocation == "" {
191 fmt.Println("No CSV location specified, using projects.csv")
192 config.CSVLocation = "projects.csv"
193 }
194
195 if config.FetchInterval < 10 {
196 fmt.Println("Fetch interval is set to", config.FetchInterval, "seconds, but the minimum is 10, using 10")
197 config.FetchInterval = 10
198 }
199
200 if config.Server.Listen == "" {
201 fmt.Println("No listen address specified, using 127.0.0.1:1313")
202 config.Server.Listen = "127.0.0.1:1313"
203 }
204
205 return nil
206}
207
208func checkCSV() error {
209 file, err := os.Open(config.CSVLocation)
210 if err != nil {
211 if os.IsNotExist(err) {
212 file, err = os.Create(config.CSVLocation)
213 if err != nil {
214 return err
215 }
216 defer file.Close()
217
218 _, err = file.WriteString("url,name,forge,running\nhttps://git.sr.ht/~amolith/earl,earl,sourcehut,v0.0.1-rc0\n")
219 if err != nil {
220 return err
221 }
222 } else {
223 return err
224 }
225 }
226 defer file.Close()
227 return nil
228}