Files
searchgit/main.go
T
claude BotandClaude Opus 5.5 1885488d05 Serveur Go : recherche GitHub par critères
- /api/search traduit les filtres en requête GitHub (langage, étoiles,
  activité, création, licence, topic, forks, archivés, good first issues)
- cache mémoire des recherches identiques (CACHE_TTL) et suivi du quota
- /api/status, /healthz, authentification basique optionnelle
- Dockerfile et docker-compose.yml

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 12:46:42 +02:00

96 lines
2.3 KiB
Go

// Searchgit: search GitHub repositories with criteria, from a web interface.
package main
import (
"context"
"crypto/subtle"
"embed"
"errors"
"io/fs"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
//go:embed web
var webFS embed.FS
func getenv(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func getenvDuration(key string, def time.Duration) time.Duration {
if d, err := time.ParseDuration(os.Getenv(key)); err == nil && d >= 0 {
return d
}
return def
}
func main() {
httpAddr := getenv("HTTP_ADDR", ":8080")
token := os.Getenv("GITHUB_TOKEN")
gh := NewGitHub(getenv("GITHUB_API", "https://api.github.com"), token, getenvDuration("CACHE_TTL", 5*time.Minute))
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
static, err := fs.Sub(webFS, "web")
if err != nil {
log.Fatal(err)
}
mux := http.NewServeMux()
(&API{gh: gh}).Routes(mux)
mux.Handle("GET /", http.FileServer(http.FS(static)))
srv := &http.Server{
Addr: httpAddr,
Handler: basicAuth(os.Getenv("AUTH_USER"), os.Getenv("AUTH_PASS"), mux),
ReadHeaderTimeout: 10 * time.Second,
BaseContext: func(net.Listener) context.Context { return ctx },
}
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
}()
if token == "" {
log.Printf("GITHUB_TOKEN not set: GitHub allows only 10 searches per minute")
}
log.Printf("web UI on %s", httpAddr)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("http: %v", err)
}
log.Println("shutdown complete")
}
// basicAuth protects the UI when AUTH_USER is set (except /healthz).
func basicAuth(user, pass string, next http.Handler) http.Handler {
if user == "" {
return next
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/healthz" {
next.ServeHTTP(w, r)
return
}
u, p, ok := r.BasicAuth()
if !ok ||
subtle.ConstantTimeCompare([]byte(u), []byte(user)) != 1 ||
subtle.ConstantTimeCompare([]byte(p), []byte(pass)) != 1 {
w.Header().Set("WWW-Authenticate", `Basic realm="searchgit"`)
http.Error(w, "authentication required", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}