- /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>
96 lines
2.3 KiB
Go
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)
|
|
})
|
|
}
|