//go:build !prod // +build !prod package main import ( "context" "crypto/rand" "embed" "encoding/hex" "fmt" "io/fs" "log" "log/slog" "mva/datastar" "mva/sqlite" "net/http" "os" "os/signal" "path/filepath" "strings" "sync" "time" ) const ( exitCodeErr = 1 exitCodeInterrupt = 2 ) //go:embed frontend var frontend embed.FS // //go:embed templates // var templates embed.FS // var templ = template.Must(template.ParseFS(templates, "templates/*.html")) // main is the entry point of the application. // Its task is to check wether all execution conditions are fullfilled. // Collecting information from the environment: flags, environment vars, configs. // Calling the run() function. func main() { fmt.Println("Developement mode") ctx := context.Background() // logging logFileName := "./mva.log" logFile, err := os.OpenFile(logFileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Printf("error opening file: %v", err) os.Exit(exitCodeErr) } defer logFile.Close() log.SetOutput(logFile) // database dbName := "./database/advendtureworks.db" db := sqlite.New(dbName) if err != nil { log.Printf("Failed to open %s database: %v", dbName, err) os.Exit(exitCodeErr) } // run the app if err := run(db, ctx); err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) os.Exit(exitCodeErr) } } // Setting up all dependencies // Creating the server (a central http handler) func run(db *sqlite.Database, ctx context.Context) error { ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) defer cancel() err := db.Open(ctx) if err != nil { log.Printf("Failed to open %s database: %v", db.Name(), err) os.Exit(exitCodeErr) } defer db.Close() server := NewServer(db) httpServer := &http.Server{ Addr: ":8080", Handler: server, } go func() { log.Printf("listening on %s\n", httpServer.Addr) if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { fmt.Fprintf(os.Stderr, "error listening and serving: %s\n", err) } }() var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() <-ctx.Done() // make a new context for the Shutdown (thanks Alessandro Rosetti) // shutdownCtx := context.Background() shutdownCtx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() if err := httpServer.Shutdown(shutdownCtx); err != nil { fmt.Fprintf(os.Stderr, "error shutting down http server: %s\n", err) } log.Printf("shut down http server on %s\n", httpServer.Addr) }() wg.Wait() return nil } // The NewServer constructor is responsible for all the top-level HTTP stuff that applies to all endpoints, like CORS, auth middleware, and logging: func NewServer(db *sqlite.Database) http.Handler { mux := http.NewServeMux() static, err := fs.Sub(frontend, "frontend") if err != nil { log.Fatal(err) } // Print the embedded filesystem tree fmt.Println("Embedded frontend filesystem tree:") err = printFSTree(static, ".", 0) if err != nil { log.Printf("Error printing filesystem tree: %v\n", err) } fmt.Println("--- End of tree ---") addRoutes( mux, static, nil, // templ ) mux.HandleFunc("GET /stream", func(w http.ResponseWriter, r *http.Request) { ticker := time.NewTicker(1000 * time.Millisecond) // original: defer ticker.Stop() defer func() { fmt.Println("defer executed") ticker.Stop() }() sse := datastar.NewSSE(w, r) for { select { case <-r.Context().Done(): slog.Debug("Client connection closed") return case <-ticker.C: bytes := make([]byte, 3) if _, err := rand.Read(bytes); err != nil { slog.Error("Error generating random bytes: ", slog.String("error", err.Error())) return } hexString := hex.EncodeToString(bytes) frag := fmt.Sprintf(`%s`, hexString, hexString, hexString) sse.MergeFragments(frag) } } }) var handler http.Handler = mux // handler = authMiddleware(handler) handler = headerMiddleware(handler) return handler } // printFSTree prints a tree-like structure of the given filesystem. func printFSTree(efs fs.FS, root string, indentLevel int) error { return fs.WalkDir(efs, root, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } // Skip the root directory itself for cleaner output if it's "." if path == "." && root == "." { return nil } indent := strings.Repeat("│ ", indentLevel) connector := "├── " // For the last item in a directory, use a different connector. // This requires knowing if it's the last item, which fs.WalkDir doesn't directly provide. // For simplicity, we'll use the same connector for all items. // A more sophisticated approach would involve reading directory entries first. fmt.Printf("%s%s%s\n", indent, connector, filepath.Base(path)) if d.IsDir() && path != root { // Avoid infinite recursion for the root itself if not handled carefully // The WalkDir function handles recursion, so we don't need to call printFSTree recursively here. // We adjust indentLevel based on path depth for visual representation. // This simple indentation based on WalkDir's path might not be perfect for deep structures // but gives a good overview. } return nil }) } // authMiddleware is a simple authentication middleware // func authMiddleware(next http.Handler) http.Handler { // return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // user, pass, ok := r.BasicAuth() // if !ok || !validateUser(user, pass) { // w.Header().Set("WWW-Authenticate", `Basic realm="Please enter your credentials"`) // http.Error(w, "Unauthorized", http.StatusUnauthorized) // return // } // next.ServeHTTP(w, r) // }) // } // authMiddleware is a simple authentication middleware func headerMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Println("Request URL:", r.URL.String()) fmt.Println("Request Headers:") for key, values := range r.Header { for _, value := range values { if key == "Referer" || strings.HasPrefix(key, "Hx") { fmt.Printf("%s: %s\n", key, value) } } } fmt.Println() next.ServeHTTP(w, r) }) } // validateUser validates the user credentials func validateUser(username, password string) bool { // In a real application, these credentials should be stored securely. return strings.EqualFold(username, "admin") && password == "password" }