Mirgate project to go
This commit is contained in:
170
internal/server/server.go
Normal file
170
internal/server/server.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
address = ":5000"
|
||||
htmlContentType = "text/html; charset=utf-8"
|
||||
notFoundTemplate = `
|
||||
<div class="tui-window">
|
||||
<fieldset class="tui-fieldset">
|
||||
<legend>Error</legend>
|
||||
<p>Route %s not found.</p>
|
||||
</fieldset>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
|
||||
// Server hosts the Luxtools HTTP handlers.
|
||||
type Server struct {
|
||||
mux *http.ServeMux
|
||||
indexPage []byte
|
||||
staticRoot string
|
||||
assetsRoot string
|
||||
|
||||
counterMu sync.Mutex
|
||||
clickCount int
|
||||
}
|
||||
|
||||
// New constructs a configured Server ready to serve requests.
|
||||
func New() (*Server, error) {
|
||||
projectRoot, err := goProjectRoot()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
indexPath := filepath.Join(projectRoot, "web", "templates", "index.html")
|
||||
staticDir := filepath.Join(projectRoot, "web", "static")
|
||||
assetsDir := filepath.Join(projectRoot, "assets")
|
||||
|
||||
indexPage, err := os.ReadFile(indexPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read index template: %w", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(staticDir); err != nil {
|
||||
return nil, fmt.Errorf("static directory check failed: %w", err)
|
||||
}
|
||||
if _, err := os.Stat(assetsDir); err != nil {
|
||||
return nil, fmt.Errorf("assets directory check failed: %w", err)
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
mux: http.NewServeMux(),
|
||||
indexPage: indexPage,
|
||||
staticRoot: staticDir,
|
||||
assetsRoot: assetsDir,
|
||||
}
|
||||
|
||||
s.registerRoutes()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Router exposes the configured http.Handler for the server.
|
||||
func (s *Server) Router() http.Handler {
|
||||
return s.mux
|
||||
}
|
||||
|
||||
// Address returns the default listen address for the server.
|
||||
func (s *Server) Address() string {
|
||||
return address
|
||||
}
|
||||
|
||||
func (s *Server) registerRoutes() {
|
||||
s.mux.Handle("/static/", http.StripPrefix("/static/", allowGetHead(http.FileServer(http.Dir(s.staticRoot)))))
|
||||
s.mux.Handle("/assets/", http.StripPrefix("/assets/", allowGetHead(http.FileServer(http.Dir(s.assetsRoot)))))
|
||||
s.mux.HandleFunc("/", s.handleRequest)
|
||||
}
|
||||
|
||||
func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/":
|
||||
s.sendHTML(w, http.StatusOK, s.indexPage)
|
||||
case "/counter":
|
||||
s.handleCounter(w, r)
|
||||
case "/time":
|
||||
s.sendHTMLString(w, http.StatusOK, renderTime(time.Now()))
|
||||
default:
|
||||
s.sendHTMLString(w, http.StatusNotFound, fmt.Sprintf(notFoundTemplate, html.EscapeString(r.URL.Path)))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleCounter(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost || r.Method == http.MethodPut {
|
||||
s.counterMu.Lock()
|
||||
s.clickCount++
|
||||
s.counterMu.Unlock()
|
||||
}
|
||||
|
||||
s.counterMu.Lock()
|
||||
count := s.clickCount
|
||||
s.counterMu.Unlock()
|
||||
|
||||
s.sendHTMLString(w, http.StatusOK, renderCounter(count))
|
||||
}
|
||||
|
||||
func (s *Server) sendHTML(w http.ResponseWriter, status int, body []byte) {
|
||||
w.Header().Set("Content-Type", htmlContentType)
|
||||
w.WriteHeader(status)
|
||||
if len(body) > 0 {
|
||||
if _, err := w.Write(body); err != nil {
|
||||
log.Printf("write response failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) sendHTMLString(w http.ResponseWriter, status int, body string) {
|
||||
s.sendHTML(w, status, []byte(body))
|
||||
}
|
||||
|
||||
func renderCounter(count int) string {
|
||||
return fmt.Sprintf(`
|
||||
<div id="counter" class="tui-panel tui-panel-inline">
|
||||
<p><strong>Clicks:</strong> %d</p>
|
||||
<button class="tui-button" hx-post="/counter" hx-target="#counter" hx-swap="outerHTML">
|
||||
Increment
|
||||
</button>
|
||||
</div>
|
||||
`, count)
|
||||
}
|
||||
|
||||
func renderTime(now time.Time) string {
|
||||
return fmt.Sprintf(`
|
||||
<div id="server-time" class="tui-panel tui-panel-inline">
|
||||
<p><strong>Server time:</strong> %s</p>
|
||||
</div>
|
||||
`, now.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
func allowGetHead(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet, http.MethodHead:
|
||||
next.ServeHTTP(w, r)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func goProjectRoot() (string, error) {
|
||||
_, currentFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
return "", errors.New("unable to determine caller information")
|
||||
}
|
||||
|
||||
dir := filepath.Dir(currentFile)
|
||||
projectRoot := filepath.Clean(filepath.Join(dir, "..", ".."))
|
||||
return projectRoot, nil
|
||||
}
|
||||
Reference in New Issue
Block a user