qkeeper/main.go
Aleksandr Berkuta 5806fe84c4 init commit
2026-03-27 20:27:56 +03:00

178 lines
4.2 KiB
Go

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)
})
}