aboutsummaryrefslogtreecommitdiff
path: root/app/fcuny-net/main.go
diff options
context:
space:
mode:
authorFranck Cuny <franck@fcuny.net>2025-09-29 17:48:39 -0700
committerFranck Cuny <franck@fcuny.net>2025-09-29 17:48:39 -0700
commit070011b105dbf63369c5389115efccd079491aae (patch)
tree34f72cbd1c25436fc8285a468a3c390bc5399cc0 /app/fcuny-net/main.go
parentadd pr-analyzer (diff)
downloadx-070011b105dbf63369c5389115efccd079491aae.tar.gz
replacing my static website with a simple web app
Diffstat (limited to '')
-rw-r--r--app/fcuny-net/main.go251
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())
+ })
+}