178 lines
4.2 KiB
Go
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)
|
|
})
|
|
}
|