2023-09-24 20:57:56 +00:00
// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
//
// SPDX-License-Identifier: Apache-2.0
2023-09-21 18:03:21 +00:00
package main
import (
"encoding/csv"
2023-09-26 19:32:07 +00:00
"errors"
2023-09-21 18:03:21 +00:00
"fmt"
2023-10-06 02:03:01 +00:00
"github.com/BurntSushi/toml"
2023-09-21 18:03:21 +00:00
"log"
"net/http"
"os"
"sort"
"strings"
"time"
"github.com/microcosm-cc/bluemonday"
2023-10-06 02:03:01 +00:00
flag "github.com/spf13/pflag"
2023-09-21 18:03:21 +00:00
)
type (
Model struct {
Projects [ ] project
}
project struct {
URL string
Name string
Forge string
Running string
Releases [ ] release
}
release struct {
Tag string
Content string
URL string
Date time . Time
}
2023-10-06 02:03:01 +00:00
Config struct {
Server server
CSVLocation string
// TODO: Make cache location configurable
// CacheLocation string
FetchInterval int
}
server struct {
Listen string
}
2023-09-21 18:03:21 +00:00
)
var (
2023-10-06 02:03:01 +00:00
flagConfig * string = flag . StringP ( "config" , "c" , "config.toml" , "Path to config file" )
config Config
2023-09-21 18:03:21 +00:00
req = make ( chan struct { } )
manualRefresh = make ( chan struct { } )
res = make ( chan [ ] project )
m = Model {
Projects : [ ] project { } ,
}
bmUGC = bluemonday . UGCPolicy ( )
bmStrict = bluemonday . StrictPolicy ( )
)
func main ( ) {
2023-10-06 02:03:01 +00:00
flag . Parse ( )
err := checkConfig ( )
2023-09-21 18:03:21 +00:00
if err != nil {
2023-10-06 02:03:01 +00:00
log . Fatalln ( err )
}
2023-09-26 20:13:18 +00:00
2023-10-06 02:03:01 +00:00
err = checkCSV ( )
if err != nil {
log . Fatalln ( err )
2023-09-21 18:03:21 +00:00
}
2023-10-06 02:03:01 +00:00
reader := csv . NewReader ( strings . NewReader ( config . CSVLocation ) )
2023-09-21 18:03:21 +00:00
records , err := reader . ReadAll ( )
if err != nil {
log . Fatalln ( err )
}
m . Projects = [ ] project { }
if len ( records ) > 0 {
for i , record := range records {
if i == 0 {
continue
}
m . Projects = append ( m . Projects , project {
URL : record [ 0 ] ,
Name : record [ 1 ] ,
Forge : record [ 2 ] ,
Running : record [ 3 ] ,
Releases : [ ] release { } ,
} )
}
}
go refreshLoop ( manualRefresh , req , res )
mux := http . NewServeMux ( )
httpServer := & http . Server {
2023-10-06 02:03:01 +00:00
Addr : config . Server . Listen ,
2023-09-21 18:03:21 +00:00
Handler : mux ,
}
mux . HandleFunc ( "/" , rootHandler )
mux . HandleFunc ( "/static" , staticHandler )
mux . HandleFunc ( "/new" , newHandler )
2023-09-26 19:32:07 +00:00
if err := httpServer . ListenAndServe ( ) ; errors . Is ( err , http . ErrServerClosed ) {
2023-09-21 18:03:21 +00:00
log . Println ( "Web server closed" )
} else {
log . Fatalln ( err )
}
}
func refreshLoop ( manualRefresh , req chan struct { } , res chan [ ] project ) {
ticker := time . NewTicker ( time . Second * 3600 )
2023-09-26 19:33:43 +00:00
fetch := func ( ) [ ] project {
2023-09-21 18:03:21 +00:00
projects := make ( [ ] project , len ( m . Projects ) )
copy ( projects , m . Projects )
for i , project := range projects {
project , err := getReleases ( project )
if err != nil {
fmt . Println ( err )
continue
}
projects [ i ] = project
}
sort . Slice ( projects , func ( i , j int ) bool { return strings . ToLower ( projects [ i ] . Name ) < strings . ToLower ( projects [ j ] . Name ) } )
return projects
}
2023-09-26 19:33:43 +00:00
projects := fetch ( )
2023-09-21 18:03:21 +00:00
for {
select {
case <- ticker . C :
2023-09-26 19:33:43 +00:00
projects = fetch ( )
2023-09-21 18:03:21 +00:00
case <- manualRefresh :
ticker . Reset ( time . Second * 3600 )
2023-09-26 19:33:43 +00:00
projects = fetch ( )
2023-09-21 18:03:21 +00:00
case <- req :
projectsCopy := make ( [ ] project , len ( projects ) )
copy ( projectsCopy , projects )
res <- projectsCopy
}
}
}
2023-10-06 02:03:01 +00:00
func checkConfig ( ) error {
file , err := os . Open ( * flagConfig )
if err != nil {
if os . IsNotExist ( err ) {
file , err = os . Create ( * flagConfig )
if err != nil {
return err
}
defer file . Close ( )
_ , 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" )
if err != nil {
return err
}
fmt . Println ( "Config file created at" , * flagConfig )
fmt . Println ( "Please edit it and restart the server" )
os . Exit ( 0 )
} else {
return err
}
}
defer file . Close ( )
_ , err = toml . DecodeFile ( * flagConfig , & config )
if err != nil {
return err
}
if config . CSVLocation == "" {
fmt . Println ( "No CSV location specified, using projects.csv" )
config . CSVLocation = "projects.csv"
}
if config . FetchInterval < 10 {
fmt . Println ( "Fetch interval is set to" , config . FetchInterval , "seconds, but the minimum is 10, using 10" )
config . FetchInterval = 10
}
if config . Server . Listen == "" {
fmt . Println ( "No listen address specified, using 127.0.0.1:1313" )
config . Server . Listen = "127.0.0.1:1313"
}
return nil
}
func checkCSV ( ) error {
file , err := os . Open ( config . CSVLocation )
if err != nil {
if os . IsNotExist ( err ) {
file , err = os . Create ( config . CSVLocation )
if err != nil {
return err
}
defer file . Close ( )
_ , err = file . WriteString ( "url,name,forge,running\nhttps://git.sr.ht/~amolith/earl,earl,sourcehut,v0.0.1-rc0\n" )
if err != nil {
return err
}
} else {
return err
}
}
defer file . Close ( )
return nil
}