package main import ( "context" "embed" "encoding/json" "flag" "fmt" "io/fs" "log" "net/http" "os" "os/signal" "runtime/debug" "strconv" "sync" "syscall" "time" "git.chandlerswift.com/chandlerswift/nau-sidewalks/sidewalk" ) //go:embed static var embeddedFiles embed.FS var buildInfoJSON []byte func main() { devel := flag.Bool("devel", false, "Serve from ./static instead of embedded") port := flag.Int("port", 8000, "Port to listen on") dataFile := flag.String("datafile", "", "File to read sidewalk data from (required)") flag.Parse() if *dataFile == "" { panic("datafile arg is required") } sidewalkData, err := os.ReadFile(*dataFile) if err != nil { panic(err) } // Decode the JSON data var sidewalks []sidewalk.Sidewalk var sidewalksMutex sync.RWMutex if err := json.Unmarshal(sidewalkData, &sidewalks); err != nil { log.Fatalf("Error decoding JSON: %v", err) } sidewalksLen := len(sidewalks) // Serve static content if *devel { http.Handle("GET /", http.FileServer(http.Dir("./static"))) } else { frontend, err := fs.Sub(embeddedFiles, "static") if err != nil { panic(err) } http.Handle("GET /", http.FileServer(http.FS(frontend))) } initializeBuildInfoJSON() http.HandleFunc("GET /api/version", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write(buildInfoJSON) }) http.HandleFunc("GET /api/sidewalks", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") sidewalksMutex.RLock() json.NewEncoder(w).Encode(sidewalks) sidewalksMutex.RUnlock() }) http.HandleFunc("POST /api/sidewalks/{id}", func(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { http.Error(w, "Could not parse form", http.StatusInternalServerError) return } sidewalkID, err := strconv.Atoi(r.PathValue("id")) if err != nil || sidewalkID < 0 || sidewalkID >= sidewalksLen { http.Error(w, "Invalid id", http.StatusBadRequest) return } condition, err := strconv.Atoi(r.Form.Get("condition")) if err != nil { http.Error(w, "Invalid condition", http.StatusBadRequest) return } sidewalksMutex.Lock() sidewalks[sidewalkID].Condition = sidewalk.Condition(condition) sidewalks[sidewalkID].LastUpdated = time.Now() sidewalksMutex.Unlock() http.Redirect(w, r, "/", http.StatusSeeOther) }) server := &http.Server{Addr: fmt.Sprintf(":%v", *port)} go func() { fmt.Printf("Serving %v sidewalks on :%v\n", sidewalksLen, *port) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("HTTP server error: %v", err) } }() // Listen for OS termination signals stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) <-stop // Block until signal is received fmt.Println("\nShutting down server...") // write out JSON sidewalksMutex.RLock() f, err := os.Create(*dataFile) if err != nil { panic(err) } defer f.Close() err = json.NewEncoder(f).Encode(sidewalks) if err != nil { panic(err) } sidewalksMutex.RUnlock() // graceful shutdown ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { log.Fatalf("Server forced to shutdown: %v", err) } } func initializeBuildInfoJSON() { rawBuildInfo, _ := debug.ReadBuildInfo() var revision, time string var dirty bool for _, buildSetting := range rawBuildInfo.Settings { if buildSetting.Key == "vcs.revision" { revision = buildSetting.Value } else if buildSetting.Key == "vcs.time" { time = buildSetting.Value } else if buildSetting.Key == "vcs.modified" { dirty = buildSetting.Value == "true" } } buildInfo := map[string]interface{}{ "GitRev": revision, "GitDirty": dirty, "GitTime": time, "GoVersion": rawBuildInfo.GoVersion, "AllBuildInfo": rawBuildInfo.String(), } var err error buildInfoJSON, err = json.Marshal(buildInfo) if err != nil { panic(err) } }