diff --git a/.gitignore b/.gitignore index a2e4405..3739dcd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,3 @@ gemini-server gemini_site .env -request_counts.json diff --git a/counter.go b/counter.go deleted file mode 100644 index fa432c3..0000000 --- a/counter.go +++ /dev/null @@ -1,137 +0,0 @@ -package main - -import ( - "encoding/json" - "log/slog" - "os" - "sync" - "time" -) - -// RequestCounter tracks request counts per path in memory with periodic snapshots -type RequestCounter struct { - counts map[string]int64 - mu sync.RWMutex - snapshotPath string - stopChan chan struct{} - wg sync.WaitGroup -} - -// NewRequestCounter creates a new request counter with periodic snapshots -func NewRequestCounter(snapshotPath string, snapshotInterval time.Duration) (*RequestCounter, error) { - c := &RequestCounter{ - counts: make(map[string]int64), - snapshotPath: snapshotPath, - stopChan: make(chan struct{}), - } - - // Load existing counts from snapshot file - if err := c.load(); err != nil { - slog.Warn("failed to load counter snapshot, starting fresh", "error", err, "path", snapshotPath) - } - - // Start periodic snapshot goroutine - c.wg.Add(1) - go c.periodicSnapshot(snapshotInterval) - - return c, nil -} - -// Increment increments the counter for a given path and returns the new count -func (c *RequestCounter) Increment(path string) int64 { - c.mu.Lock() - defer c.mu.Unlock() - c.counts[path]++ - return c.counts[path] -} - -// Get returns the current count for a given path -func (c *RequestCounter) Get(path string) int64 { - c.mu.RLock() - defer c.mu.RUnlock() - return c.counts[path] -} - -func (c *RequestCounter) GetTotal() int64 { - c.mu.Lock() - defer c.mu.Unlock() - var total int64 - for _, count := range c.counts { - total += count - } - return total -} - -// load reads the snapshot file and loads counts into memory -func (c *RequestCounter) load() error { - data, err := os.ReadFile(c.snapshotPath) - if err != nil { - if os.IsNotExist(err) { - return nil // File doesn't exist yet, that's okay - } - return err - } - - c.mu.Lock() - defer c.mu.Unlock() - - if err := json.Unmarshal(data, &c.counts); err != nil { - return err - } - - slog.Info("loaded request counter snapshot", "paths", len(c.counts), "file", c.snapshotPath) - return nil -} - -// snapshot writes current counts to disk -func (c *RequestCounter) snapshot() error { - c.mu.RLock() - data, err := json.MarshalIndent(c.counts, "", " ") - c.mu.RUnlock() - - if err != nil { - return err - } - - // Write to temp file first, then rename for atomicity - tempPath := c.snapshotPath + ".tmp" - if err := os.WriteFile(tempPath, data, 0644); err != nil { - return err - } - - if err := os.Rename(tempPath, c.snapshotPath); err != nil { - return err - } - - slog.Debug("saved request counter snapshot", "paths", len(c.counts), "file", c.snapshotPath) - return nil -} - -// periodicSnapshot runs in a goroutine and saves snapshots at regular intervals -func (c *RequestCounter) periodicSnapshot(interval time.Duration) { - defer c.wg.Done() - ticker := time.NewTicker(interval) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - if err := c.snapshot(); err != nil { - slog.Error("failed to save counter snapshot", "error", err) - } - case <-c.stopChan: - // Final snapshot before shutdown - if err := c.snapshot(); err != nil { - slog.Error("failed to save final counter snapshot", "error", err) - } - return - } - } -} - -// Close stops the periodic snapshot goroutine and saves a final snapshot -func (c *RequestCounter) Close() error { - close(c.stopChan) - c.wg.Wait() - return nil -} diff --git a/dev.sh b/dev.sh index 5061467..a36cc39 100755 --- a/dev.sh +++ b/dev.sh @@ -4,4 +4,4 @@ set -e source .env -fd | entr -r go run . -cert=./keys/localhost.crt.pem -key=./keys/localhost_key.pem -host=localhost:8080 +fd | entr -r go run main.go -cert=./keys/localhost.crt.pem -key=./keys/localhost_key.pem -host=localhost:8080 diff --git a/main.go b/main.go index 5dad5fc..b19d8eb 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "database/sql" + "errors" "flag" "fmt" "log/slog" @@ -25,17 +26,11 @@ type MainHandler struct { blog microblog.Handler gemlog gemlog.Handler guestbook guestbook.Handler - db *sql.DB - counter *RequestCounter } func (h MainHandler) ServeGemini(w gemini.ResponseWriter, req *gemini.Request) { - // Increment request counter - count := h.counter.Increment(req.URL.Path) - slog.Info("gemini request", "path", req.URL.Path, - "count", count, "user", strings.Join(userName(req), " ")) // Check if this is a blog request @@ -57,40 +52,37 @@ func (h MainHandler) ServeGemini(w gemini.ResponseWriter, req *gemini.Request) { switch req.URL.Path { case "/": + gemini.ServeFileName("pages/home.gmi", "text/gemini")(w, req) + // err := w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini") + // requireNoError(err) + // _, err = w.WriteBody([]byte("Hello, world!")) + // requireNoError(err) + case "/user": + if req.Certificate() == nil { + w.WriteStatusMsg(gemini.StatusCertRequired, "Authentication Required") + return + } w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini") - data, err := os.ReadFile("pages/home.gmi") + w.WriteBody([]byte(req.Certificate().Subject.CommonName)) + case "/die": + requireNoError(errors.New("must die")) + case "/file": + gemini.ServeFileName("cmd/example/hello.gmi", "text/gemini")(w, req) + case "/post": + if req.URL.Scheme != gemini.SchemaTitan { + w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini") + w.WriteBody([]byte("Use titan scheme to upload data")) + return + } + payload, err := req.ReadTitanPayload() requireNoError(err) - page := string(data) - var content strings.Builder - content.WriteString(page) - content.WriteString(fmt.Sprintf("\n\n------ stats: total requests served %d, this page %d ------", h.counter.GetTotal(), h.counter.Get(req.URL.Path))) - w.WriteBody([]byte(content.String())) - // case "/user": - // if req.Certificate() == nil { - // w.WriteStatusMsg(gemini.StatusCertRequired, "Authentication Required") - // return - // } - // w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini") - // w.WriteBody([]byte(req.Certificate().Subject.CommonName)) - // case "/die": - // requireNoError(errors.New("must die")) - // case "/file": - // gemini.ServeFileName("cmd/example/hello.gmi", "text/gemini")(w, req) - // case "/post": - // if req.URL.Scheme != gemini.SchemaTitan { - // w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini") - // w.WriteBody([]byte("Use titan scheme to upload data")) - // return - // } - // payload, err := req.ReadTitanPayload() - // requireNoError(err) - // w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini") - // w.WriteBody([]byte("Titan Upload Parameters\r\n")) - // w.WriteBody([]byte(fmt.Sprintf("Upload MIME Type: %s\r\n", req.Titan.Mime))) - // w.WriteBody([]byte(fmt.Sprintf("Token: %s\r\n", req.Titan.Token))) - // w.WriteBody([]byte(fmt.Sprintf("Size: %v\r\n", req.Titan.Size))) - // w.WriteBody([]byte("Payload:\r\n")) - // w.WriteBody(payload) + w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini") + w.WriteBody([]byte("Titan Upload Parameters\r\n")) + w.WriteBody([]byte(fmt.Sprintf("Upload MIME Type: %s\r\n", req.Titan.Mime))) + w.WriteBody([]byte(fmt.Sprintf("Token: %s\r\n", req.Titan.Token))) + w.WriteBody([]byte(fmt.Sprintf("Size: %v\r\n", req.Titan.Size))) + w.WriteBody([]byte("Payload:\r\n")) + w.WriteBody(payload) default: w.WriteStatusMsg(gemini.StatusNotFound, req.URL.Path) @@ -140,21 +132,11 @@ func main() { flag.StringVar(&key, "key", "server.key.pem", "private key associated with certificate file") flag.Parse() - // Initialize request counter with 30-second snapshot interval - counter, err := NewRequestCounter("request_counts.json", 30*time.Second) - if err != nil { - slog.Error("failed to initialize request counter", "error", err) - os.Exit(1) - } - defer counter.Close() - pbClient := pocketbase.NewPocketBaseClient() handler := MainHandler{ blog: microblog.NewHandler(pbClient), gemlog: gemlog.NewHandler(), guestbook: guestbook.NewGuestBook(db), - db: db, - counter: counter, } err = gemini.ListenAndServe(host, cert, key, gemini.TrapPanic(handler.ServeGemini)) diff --git a/pages/home.gmi b/pages/home.gmi index 141f934..492653d 100644 --- a/pages/home.gmi +++ b/pages/home.gmi @@ -32,11 +32,6 @@ What I'm currently working on: ## Site updates -== 2025.11.07 == - -Added request counter. Scroll to bottom of this page to check it out. - -=> /codeview/raw/personal-gemini-capsule/src/branch/main/counter.go Code for the counter here == 2025.10.07 ==