Files
MRLokopandClaude Code d74ad09bf3 List one directory level in the UI instead of walking the whole store
dirBrowse rendered a single directory level but paid for a full
recursive listing of the repo — on CephFS-backed LocalFS that is a
walk of the entire storage root plus a sidecar read per object, which
took Ceph down on large repos. Storage grows a ListDir primitive
(os.ReadDir on localfs, Delimiter on S3) and the UI and the debian
dists fallback use it; the recursive List stays for the docker page
and now starts its walk at the requested prefix.

The per-vrepo storage gauges on /metrics did the same full listing on
every scrape; they are removed — the backend's own accounting (du,
bucket metrics) covers capacity.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 09:31:02 +03:00

139 lines
4.8 KiB
Go

package main
import (
"context"
"flag"
"log/slog"
"net"
"net/http"
"os"
"time"
"tiny-proxy/internal/config"
"tiny-proxy/internal/distro"
"tiny-proxy/internal/docker"
"tiny-proxy/internal/github"
"tiny-proxy/internal/maven"
"tiny-proxy/internal/metrics"
"tiny-proxy/internal/proxycache"
"tiny-proxy/internal/storage"
"tiny-proxy/internal/tracing"
"tiny-proxy/internal/ui"
"tiny-proxy/internal/upstream"
)
func main() {
configPath := flag.String("config", "config.yaml", "path to config file")
flag.Parse()
cfg, err := config.Load(*configPath)
if err != nil {
slog.Error("load config", "err", err)
os.Exit(1)
}
// From here on every log record carries the trace id of the request that
// caused it, so one grep follows a client request through the upstreams
// it fanned out to.
slog.SetDefault(tracing.NewLogger(os.Stderr, cfg.Log.Level, cfg.Log.Format))
ctx := context.Background()
reg := metrics.NewRegistry()
var store storage.Store
switch cfg.Storage.Type {
case "none":
// No cache: every request is proxied straight to the upstream.
store = storage.NullStore{}
case "s3":
store, err = storage.NewS3(ctx, cfg.Storage.S3)
default:
var local *storage.LocalFS
local, err = storage.NewLocalFS(cfg.Storage.LocalFS.Root)
if local != nil {
local.OnCorrupt = func(string) { reg.IncCorruptEntry() }
store = local
}
}
if err != nil {
slog.Error("init storage", "err", err)
os.Exit(1)
}
// One proxy/auth-aware HTTP client per vrepo upstream (maven, debian and
// pacman vrepos may have several, tried in order).
clients := map[string][]*http.Client{}
for _, v := range cfg.VRepos {
// Registering up front keeps every configured series present from
// the first scrape — a mirror that has served nothing yet reads as
// zero rather than as a gap — and lets each client hold its own
// counters directly instead of looking them up per request.
reg.RegisterVRepo(v.Name, string(v.Type))
for _, u := range v.AllUpstreams() {
c, err := upstream.NewClient(u, upstream.Options{
VRepo: v.Name,
Log: cfg.Log.LogUpstream(),
Metrics: reg,
Stats: reg.RegisterUpstream(v.Name, u.URL),
})
if err != nil {
slog.Error("init upstream client", "vrepo", v.Name, "url", u.URL, "err", err)
os.Exit(1)
}
clients[v.Name] = append(clients[v.Name], c)
}
}
// Docker, github and gitea vrepos are single-upstream.
first := map[string]*http.Client{}
for name, cs := range clients {
first[name] = cs[0]
}
cache := proxycache.New(store)
cache.Metrics = reg
mux := http.NewServeMux()
mux.Handle("/v2/", metrics.CountRequests(reg, "docker", docker.NewHandler(cfg.VRepos, first, cache)))
mux.Handle("/maven/", metrics.CountRequests(reg, "maven", maven.NewHandler(cfg.VRepos, clients, cache)))
mux.Handle("/github/", metrics.CountRequests(reg, "github", github.NewHandler(cfg.VRepos, first, cache)))
mux.Handle("/gitea/", metrics.CountRequests(reg, "gitea", github.NewHandler(cfg.VRepos, first, cache)))
mux.Handle("/debian/", metrics.CountRequests(reg, "debian", distro.NewHandler(distro.Debian, cfg.VRepos, clients, cache)))
mux.Handle("/pacman/", metrics.CountRequests(reg, "pacman", distro.NewHandler(distro.Pacman, cfg.VRepos, clients, cache)))
mux.Handle("/ui/", ui.NewHandler(cfg.VRepos, store, clients).Routes())
mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/ui/", http.StatusFound)
})
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok\n"))
})
mux.Handle("GET /metrics", metrics.NewHandler(reg, cfg.Metrics.Token))
srv := &http.Server{
Addr: cfg.Listen,
// Wrapping the whole mux, so /ui/, /metrics and /healthz get trace
// ids and access logs too.
Handler: tracing.Middleware(cfg.Log.LogRequests(), reg, mux),
ReadHeaderTimeout: 10 * time.Second,
}
for _, v := range cfg.VRepos {
urls := make([]string, 0, len(v.AllUpstreams()))
for _, u := range v.AllUpstreams() {
urls = append(urls, u.URL)
}
slog.Info("vrepo configured", "name", v.Name, "type", v.Type, "upstreams", urls, "auth", v.Auth.Mode)
}
// Serving on our own listener rather than ListenAndServe so client
// connections can be counted at the socket: the client byte totals then
// include request lines, headers and chunked framing instead of payload
// alone. Bound before the ready line, so a taken port reports as a
// failure to start rather than after announcing success.
ln, err := net.Listen("tcp", cfg.Listen)
if err != nil {
slog.Error("listen", "addr", cfg.Listen, "err", err)
os.Exit(1)
}
slog.Info("listening", "addr", cfg.Listen, "storage", cfg.Storage.Type,
"log_level", cfg.Log.Level, "log_format", cfg.Log.Format)
if err := srv.Serve(reg.CountConnections(ln)); err != nil {
slog.Error("server stopped", "err", err)
os.Exit(1)
}
}