main.go

  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}