eclipse/cmd/eclipse-httpd/main.go

178 lines
4.5 KiB
Go

// Copyright (C) 2023-2024 Umorpha Systems
// SPDX-License-Identifier: AGPL-3.0-or-later
package main
import (
"context"
_ "embed"
"fmt"
"html/template"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/datawire/ocibuild/pkg/cliutil"
"github.com/spf13/cobra"
source "git.mothstuff.lol/lukeshu/eclipse"
"git.mothstuff.lol/lukeshu/eclipse/lib/config"
"git.mothstuff.lol/lukeshu/eclipse/lib/store"
)
func main() {
argparser := &cobra.Command{
Use: os.Args[0] + " [flags]",
Short: "HTTP server",
SilenceErrors: true, // we'll handle this ourselves after .ExecuteContext()
SilenceUsage: true, // FlagErrorFunc will handle this
CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: true,
},
}
argparser.SetFlagErrorFunc(cliutil.FlagErrorFunc)
argparser.SetHelpTemplate(cliutil.HelpTemplate)
var cfgFile string
argparser.Flags().StringVar(&cfgFile, "config", "/etc/eclipse.yml",
"Config file to use")
argparser.MarkFlagFilename("config", "yml", "yaml")
var socket string
argparser.Flags().StringVar(&socket, "socket", "tcp::8080",
"the socket to listen on; a \"family:address\" pair, where the\n"+
"\"family\" is one of \"tcp\", \"tcp4\", \"tcp6\", \"unix\", \"unixpacket\"\n"+
"(the usual Go net.Listen() network names), or \"fd\" (to listen\n"+
"on an already-open file descriptor). The \"address\" part for\n"+
"the \"fd\" family is one of\n"+
" 1. a file descriptor number (or the special names \"stdin\",\n"+
" \"stdout\", or \"stderr\" to refer to 0, 1, or 2\n"+
" respectively),\n"+
" 2. \"systemd:N\" to refer to the Nth file descriptor passed in\n"+
" via systemd (or \"systemd\" as shorthand for \"systemd:0\")\n"+
" (see sd_listend_fds(3)),\n"+
" 3. \"systemd:NAME\" to refer to a named file descriptor passed\n"+
" in via systemd (see sd_listend_fds_with_names(3)).\n")
argparser.RunE = func(cmd *cobra.Command, args []string) error {
stype, saddr, ok := strings.Cut(socket, ":")
if !ok {
return cliutil.FlagErrorFunc(cmd, fmt.Errorf("invalid address: %q", socket))
}
return Serve(cfgFile, stype, saddr)
}
ctx := context.Background()
if err := argparser.ExecuteContext(ctx); err != nil {
fmt.Fprintf(os.Stderr, "%v: error: %v\n", argparser.CommandPath(), err)
os.Exit(1)
}
}
func Serve(cfgFile, stype, saddr string) (err error) {
maybeSetErr := func(_err error) {
if err == nil && _err != nil {
err = _err
}
}
cfg, err := config.Load(cfgFile)
if err != nil {
return err
}
store, err := store.Open(filepath.Join(cfg.Daemon.StateDir, "state.sqlite"))
if err != nil {
return err
}
defer func() { maybeSetErr(store.Close()) }()
sock, err := netListen(stype, saddr)
if err != nil {
return err
}
state := &ServerState{
db: store,
}
router := http.NewServeMux()
router.HandleFunc("/", serveStatic)
router.HandleFunc("/jobs/", state.serveJobs)
log.Printf("Serving on %v...", sock.Addr())
return http.Serve(sock, router)
}
//go:embed index.html
var fileIndex []byte
//go:embed style.css
var fileStyle []byte
//go:embed favicon.ico
var fileFavicon []byte
func serveStatic(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "HTTP 405: only GET is supported", http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/":
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write(fileIndex)
case "/style.css":
w.Header().Set("Content-Type", "text/css")
_, _ = w.Write(fileStyle)
case "/favicon.ico":
w.Header().Set("Content-Type", "image/vnd.microsoft.icon")
_, _ = w.Write(fileFavicon)
case "/eclipse.tar":
w.Header().Set("Content-Type", "application/x-tar")
_, _ = w.Write(source.Tarball)
default:
http.NotFound(w, r)
return
}
}
type ServerState struct {
db *store.Store
}
//go:embed jobs.html.tmpl
var fileJobsTmpl string
var jobsTmpl = template.Must(template.New("jobs.html.tmpl").
Parse(fileJobsTmpl))
func (o *ServerState) serveJobs(w http.ResponseWriter, r *http.Request) {
rest := strings.TrimPrefix(r.URL.Path, "/jobs/")
if rest == "" {
switch r.Method {
case http.MethodGet:
jobs, err := o.db.ListJobs()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
jobsTmpl.Execute(w, map[string]any{
"Jobs": jobs,
})
case http.MethodPost:
// TODO
default:
http.Error(w, "HTTP 405: only GET and POST are supported", http.StatusMethodNotAllowed)
return
}
} else {
// TODO
}
}