package main import ( "compress/gzip" "embed" "encoding/json" "io" "io/fs" "log" "mime" "net/http" "os" "path/filepath" "strings" qrcode "github.com/skip2/go-qrcode" ) //go:embed static var rawFS embed.FS func main() { dbPath := os.Getenv("DB_PATH") if dbPath == "" { dbPath = "queues.db" } reg, err := openRegistry(dbPath) if err != nil { log.Fatalf("open registry: %v", err) } appURL := os.Getenv("APP_URL") staticFS, err := fs.Sub(rawFS, "static") if err != nil { log.Fatal(err) } mux := http.NewServeMux() // ---- About / home page ---- mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { serveFile(w, r, staticFS, "about.html") }) // ---- Help / info page ---- mux.HandleFunc("GET /help.html", func(w http.ResponseWriter, r *http.Request) { serveFile(w, r, staticFS, "help.html") }) // ---- Queue list (JSON) ---- mux.HandleFunc("GET /queues", func(w http.ResponseWriter, r *http.Request) { list := reg.List() b, _ := json.Marshal(list) w.Header().Set("Content-Type", "application/json") w.Write(b) }) // ---- Create queue ---- mux.HandleFunc("POST /new", func(w http.ResponseWriter, r *http.Request) { name := sanitize(strings.TrimSpace(r.FormValue("name")), 100) if name == "" { name = "Очередь" } inst, err := reg.Create(name) if err != nil { http.Error(w, "не удалось создать очередь", http.StatusInternalServerError) return } http.Redirect(w, r, "/q/"+inst.ID, http.StatusSeeOther) }) // ---- Queue page ---- mux.HandleFunc("GET /q/{id}", func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if reg.Get(id) == nil { http.Redirect(w, r, "/", http.StatusSeeOther) return } serveFile(w, r, staticFS, "index.html") }) // ---- Queue WebSocket ---- mux.HandleFunc("GET /q/{id}/ws", func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") inst := reg.Get(id) if inst == nil { http.NotFound(w, r) return } inst.hub.wsHandler(w, r) }) // ---- Queue QR code ---- mux.HandleFunc("GET /q/{id}/qr.png", func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if reg.Get(id) == nil { http.NotFound(w, r) return } url := appURL if url == "" { scheme := "http" if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" { scheme = "https" } url = scheme + "://" + r.Host log.Printf("APP_URL not set, using derived URL: %s", url) } url += "/q/" + id png, err := qrcode.Encode(url, qrcode.Medium, 256) if err != nil { http.Error(w, "qr generation failed", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "image/png") w.Header().Set("Cache-Control", "public, max-age=86400") w.Write(png) }) addr := ":8080" log.Printf("queue server listening on %s APP_URL=%q DB=%q", addr, appURL, dbPath) log.Fatal(http.ListenAndServe(addr, mux)) } // serveFile reads a file from an fs.FS and writes it with gzip if supported. func serveFile(w http.ResponseWriter, r *http.Request, fsys fs.FS, name string) { f, err := fsys.Open(name) if err != nil { http.NotFound(w, r) return } defer f.Close() data, err := io.ReadAll(f) if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } ct := mime.TypeByExtension(filepath.Ext(name)) if ct == "" { ct = http.DetectContentType(data) } w.Header().Set("Content-Type", ct) if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { w.Header().Set("Content-Encoding", "gzip") gz, _ := gzip.NewWriterLevel(w, gzip.BestSpeed) defer gz.Close() gz.Write(data) } else { w.Write(data) } } // ---- Gzip middleware (for future use on static file handler) ---- type gzipResponseWriter struct { http.ResponseWriter gz *gzip.Writer } func (g *gzipResponseWriter) Write(b []byte) (int, error) { return g.gz.Write(b) } func gzipMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { next.ServeHTTP(w, r) return } w.Header().Set("Content-Encoding", "gzip") w.Header().Del("Content-Length") gz, _ := gzip.NewWriterLevel(w, gzip.BestSpeed) defer gz.Close() next.ServeHTTP(&gzipResponseWriter{w, gz}, r) }) }