diff --git a/caddy/pmtiles_proxy.go b/caddy/pmtiles_proxy.go new file mode 100644 index 0000000..0313ed0 --- /dev/null +++ b/caddy/pmtiles_proxy.go @@ -0,0 +1,109 @@ +package pmtiles_proxy + +import ( + "fmt" + "io" + "log" + "net/http" + "strconv" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/protomaps/go-pmtiles/pmtiles" + "go.uber.org/zap" + "time" +) + +func init() { + caddy.RegisterModule(Middleware{}) + httpcaddyfile.RegisterHandlerDirective("pmtiles_proxy", parseCaddyfile) +} + +type Middleware struct { + Bucket string `json:"bucket"` + CacheSize int `json:"cache_size"` + logger *zap.Logger + server *pmtiles.Server +} + +func (Middleware) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "http.handlers.pmtiles_proxy", + New: func() caddy.Module { return new(Middleware) }, + } +} + +func (m *Middleware) Provision(ctx caddy.Context) error { + m.logger = ctx.Logger() + logger := log.New(io.Discard, "", log.Ldate) + prefix := "." // serve only the root of the bucket for now, at the root route of Caddyfile + server, err := pmtiles.NewServer(m.Bucket, prefix, logger, 64, "", "https://example.com") + if err != nil { + return err + } + m.server = server + server.Start() + return nil +} + +func (m *Middleware) Validate() error { + if m.Bucket == "" { + return fmt.Errorf("no bucket") + } + if m.CacheSize <= 0 { + m.CacheSize = 64 + } + return nil +} + +func (m Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { + start := time.Now() + status_code, headers, body := m.server.Get(r.Context(), r.URL.Path) + for k, v := range headers { + w.Header().Set(k, v) + } + w.WriteHeader(status_code) + w.Write(body) + m.logger.Info("response", zap.Int("status", status_code), zap.String("path", r.URL.Path), zap.Duration("duration", time.Since(start))) + + return next.ServeHTTP(w, r) +} + +func (m *Middleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "bucket": + if !d.Args(&m.Bucket) { + return d.ArgErr() + } + case "cache_size": + var cacheSize string + if !d.Args(&cacheSize) { + return d.ArgErr() + } + num, err := strconv.Atoi(cacheSize) + if err != nil { + return d.ArgErr() + } + m.CacheSize = num + } + } + } + return nil +} + +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + var m Middleware + err := m.UnmarshalCaddyfile(h.Dispenser) + return m, err +} + +var ( + _ caddy.Provisioner = (*Middleware)(nil) + _ caddy.Validator = (*Middleware)(nil) + _ caddyhttp.MiddlewareHandler = (*Middleware)(nil) + _ caddyfile.Unmarshaler = (*Middleware)(nil) +)