diff options
| author | Franck Cuny <franck@fcuny.net> | 2025-09-29 17:48:39 -0700 |
|---|---|---|
| committer | Franck Cuny <franck@fcuny.net> | 2025-09-29 17:48:39 -0700 |
| commit | 070011b105dbf63369c5389115efccd079491aae (patch) | |
| tree | 34f72cbd1c25436fc8285a468a3c390bc5399cc0 /app/fcuny-net/main.go | |
| parent | add pr-analyzer (diff) | |
| download | x-070011b105dbf63369c5389115efccd079491aae.tar.gz | |
replacing my static website with a simple web app
Diffstat (limited to '')
| -rw-r--r-- | app/fcuny-net/main.go | 251 |
1 files changed, 251 insertions, 0 deletions
diff --git a/app/fcuny-net/main.go b/app/fcuny-net/main.go new file mode 100644 index 0000000..8976964 --- /dev/null +++ b/app/fcuny-net/main.go @@ -0,0 +1,251 @@ +package main + +import ( + "context" + "embed" + "fmt" + "html/template" + "io/fs" + "log" + "net/http" + "os" + "os/signal" + "path" + "runtime" + "syscall" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" + "rsc.io/markdown" +) + +//go:embed content/* +var contentFiles embed.FS + +//go:embed static/* +var staticFiles embed.FS + +//go:embed templates/* +var templateFiles embed.FS + +const ( + domain = "fcuny.net" + forgeDomain = "code.fcuny.net" + forgeUser = "fcuny" +) + +type PageData struct { + Title string + Content template.HTML +} + +var ( + goGetReqs = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "goget_requests_total", + Help: "go get requests processed, by repository name.", + }, []string{"name"}) + + staticReqs = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "static_requests_total", + Help: "HTTP requests served from the FS.", + }, []string{"path"}) + + modules = []string{"x"} +) + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + + metricsMux := http.NewServeMux() + metricsMux.Handle("/metrics", promhttp.Handler()) + metricsMux.Handle("/.info", info()) + + metricsServer := &http.Server{ + Addr: ":9091", + Handler: metricsMux, + ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, + } + + go func() { + log.Println("Starting metrics server on :9091") + if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Printf("Metrics server error: %v", err) + } + }() + + s := &http.Server{ + Addr: ":8070", + Handler: handler(), + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 1 * time.Minute, + } + + go func() { + log.Println("Starting main server on :8070") + if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Printf("Main server error: %v", err) + } + }() + + <-ctx.Done() + log.Println("Shutdown signal received, starting graceful shutdown...") + + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer shutdownCancel() + + var shutdownErr error + + log.Println("Shutting down main server...") + if err := s.Shutdown(shutdownCtx); err != nil { + log.Printf("Main server shutdown error: %v", err) + shutdownErr = err + } + + log.Println("Shutting down metrics server...") + if err := metricsServer.Shutdown(shutdownCtx); err != nil { + log.Printf("Metrics server shutdown error: %v", err) + if shutdownErr == nil { + shutdownErr = err + } + } + + if shutdownErr != nil { + log.Printf("Shutdown completed with errors") + os.Exit(1) + } + + log.Println("Shutdown completed successfully") +} + +func handler() http.Handler { + mux := http.NewServeMux() + + staticFS, _ := fs.Sub(staticFiles, "static") + fileServer := http.FileServer(http.FS(staticFS)) + mux.Handle("/static/", http.StripPrefix("/static/", fileServer)) + + mux.HandleFunc("/", homeHandler) + mux.HandleFunc("/resume", resumeHandler) + + for _, name := range modules { + module := "/" + name + mux.Handle( + module+"/", + goImportHandler(module, "https://"+forgeDomain+"/"+forgeUser+"/"+name), + ) + } + + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != http.MethodHead { + status := http.StatusMethodNotAllowed + http.Error(rw, http.StatusText(status), status) + return + } + mux.ServeHTTP(rw, r) + }) +} + +func homeHandler(w http.ResponseWriter, r *http.Request) { + markdownContent, err := contentFiles.ReadFile("content/home.md") + if err != nil { + log.Printf("Error reading home markdown: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + parser := markdown.Parser{} + doc := parser.Parse(string(markdownContent)) + + htmlContent := markdown.ToHTML(doc) + + tmplContent, err := templateFiles.ReadFile("templates/home.html") + if err != nil { + log.Printf("Error reading template: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + tmpl, err := template.New("layout").Parse(string(tmplContent)) + if err != nil { + log.Printf("Error parsing template: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + data := PageData{ + Title: "Franck Cuny", + Content: template.HTML(htmlContent), + } + + w.Header().Set("Content-Type", "text/html; charset=UTF-8") + if err := tmpl.Execute(w, data); err != nil { + log.Printf("Error executing template: %v", err) + } + staticReqs.WithLabelValues(path.Clean(r.URL.Path)).Inc() +} + +func resumeHandler(w http.ResponseWriter, r *http.Request) { + markdownContent, err := contentFiles.ReadFile("content/resume.md") + if err != nil { + log.Printf("Error reading resume markdown: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + parser := markdown.Parser{} + doc := parser.Parse(string(markdownContent)) + + htmlContent := markdown.ToHTML(doc) + + tmplContent, err := templateFiles.ReadFile("templates/resume.html") + if err != nil { + log.Printf("Error reading template: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + tmpl, err := template.New("layout").Parse(string(tmplContent)) + if err != nil { + log.Printf("Error parsing template: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + data := PageData{ + Title: "Work Experience", + Content: template.HTML(htmlContent), + } + + w.Header().Set("Content-Type", "text/html; charset=UTF-8") + if err := tmpl.Execute(w, data); err != nil { + log.Printf("Error executing template: %v", err) + } + staticReqs.WithLabelValues(path.Clean(r.URL.Path)).Inc() +} + +func goImportHandler(module, repo string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + goGetReqs.WithLabelValues(module).Inc() + + if r.URL.Query().Get("go-get") == "1" { + w.Header().Set("Content-Type", "text/html; charset=UTF-8") + importPath := domain + module + if _, err := fmt.Fprintf(w, `<html><head><meta name="go-import" content="%s git %s"></head></html>`, importPath, repo); err != nil { + log.Printf("Error writing response: %v", err) + } + return + } + + http.Redirect(w, r, repo, http.StatusFound) + }) +} + +func info() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Go version: %s\n", runtime.Version()) + }) +} |
