Poll sync cheaply and make the server deployable
Polling re-downloaded the whole store, photos included, every five
seconds. The app now checks a tiny version endpoint and only fetches the
payload when it actually moved, and the server gained request logging,
graceful shutdown, and self-hosting docs so it can run on a VPS instead
of only on localhost.
💘 Generated with Crush
Assisted-by: Crush:kimi-k3
This commit is contained in:
+68
-1
@@ -6,6 +6,7 @@
|
||||
//
|
||||
// POST /api/codes -> {"code":"1234-5678"} (create group)
|
||||
// GET /api/sync/{code} -> {"version":N,"data":{...}} (404 if unknown)
|
||||
// GET /api/sync/{code}/version -> {"version":N} (cheap poll)
|
||||
// PUT /api/sync/{code} -> {"version":N+1} (upsert payload)
|
||||
// GET /healthz -> 200
|
||||
//
|
||||
@@ -16,6 +17,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
@@ -23,7 +25,9 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
@@ -69,13 +73,55 @@ func main() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("POST /api/codes", s.createCode)
|
||||
mux.HandleFunc("GET /api/sync/{code}", s.getSync)
|
||||
mux.HandleFunc("GET /api/sync/{code}/version", s.getVersion)
|
||||
mux.HandleFunc("PUT /api/sync/{code}", s.putSync)
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: withLogging(mux),
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
// Shut down cleanly so a redeploy (systemd restart, container stop) never
|
||||
// cuts off an in-flight push and leaves a client thinking it failed.
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-stop
|
||||
log.Println("shutting down")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
_ = server.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
log.Printf("prompt-sync listening on %s", addr)
|
||||
log.Fatal(http.ListenAndServe(addr, mux))
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("serve: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// withLogging keeps a one-line trail of method, path, status and duration so a
|
||||
// self-hosted deployment can be debugged from journalctl or docker logs.
|
||||
func withLogging(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
started := time.Now()
|
||||
recorder := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
|
||||
next.ServeHTTP(recorder, r)
|
||||
log.Printf("%s %s %d %s", r.Method, r.URL.Path, recorder.status, time.Since(started).Round(time.Millisecond))
|
||||
})
|
||||
}
|
||||
|
||||
type statusRecorder struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
}
|
||||
|
||||
func (r *statusRecorder) WriteHeader(status int) {
|
||||
r.status = status
|
||||
r.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
// createCode generates a unique 8-digit code, formats it as 1234-5678, and
|
||||
@@ -136,6 +182,27 @@ func (s *Server) getSync(w http.ResponseWriter, r *http.Request) {
|
||||
version, updatedAt.UTC().Format(time.RFC3339), data)
|
||||
}
|
||||
|
||||
// getVersion is the cheap poll: clients ask for the version every few seconds
|
||||
// and only download the payload (which can carry base64 images) when it moved.
|
||||
func (s *Server) getVersion(w http.ResponseWriter, r *http.Request) {
|
||||
code := r.PathValue("code")
|
||||
if !codePattern.MatchString(code) {
|
||||
writeError(w, http.StatusBadRequest, "code must look like 1234-5678")
|
||||
return
|
||||
}
|
||||
var version int64
|
||||
err := s.db.QueryRow(`SELECT version FROM sync_groups WHERE code = $1`, code).Scan(&version)
|
||||
if err == sql.ErrNoRows {
|
||||
writeError(w, http.StatusNotFound, "unknown code")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "database error")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]int64{"version": version})
|
||||
}
|
||||
|
||||
func (s *Server) putSync(w http.ResponseWriter, r *http.Request) {
|
||||
code := r.PathValue("code")
|
||||
if !codePattern.MatchString(code) {
|
||||
|
||||
Reference in New Issue
Block a user