packages.wenpai.net/internal/http/downloads.go
Ben Word 7cb8fef01b
WP Packages rename (#42)
* Update all import paths

* Rename directory cmd/wpcomposer/ → cmd/wppackages/

* Rename import alias wpcomposergo → wppackagesgo in main.go and migrate_test.go

* Makefile — binary name wpcomposer → wppackages

* Update Air path

* Global replace repo.wp-composer.com → repo.wp-packages.org

* Global replace cdn.wp-composer.com → cdn.wp-packages.org

* Global replace wp-composer.com → wp-packages.org (remaining)

* Composer repo key in templates/docs: repositories.wp-composer → repositories.wp-packages

* Rename columns on the existing schema

* Update all Go code referencing these column names

* Routes & SEO

* Templates & front-end

* Admin UI

* Documentation

* CI/CD

* Config defaults

* Rename role directory

* Rename all systemd template files inside the role

* Update contents of all .j2 templates — service names, binary paths, descriptions

* Update tasks/main.yml and handlers/main.yml in the role

* Update deploy/ansible/roles/app/tasks/main.yml and deploy.yml

* Update deploy/ansible/group_vars/production/main.yml

* Additional renames/fixes

* Additional renames/fixes

* Additional renames/fixes

* not needed
2026-03-19 11:50:12 -05:00

75 lines
1.9 KiB
Go

package http
import (
"encoding/json"
"net"
"net/http"
"github.com/roots/wp-packages/internal/app"
"github.com/roots/wp-packages/internal/telemetry"
)
type notifyBatchRequest struct {
Downloads []downloadEntry `json:"downloads"`
}
type downloadEntry struct {
Name string `json:"name"`
Version string `json:"version"`
}
const maxDownloadsRequestBodyBytes = 1 << 20 // 1 MiB
func handleDownloads(a *app.App) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req notifyBatchRequest
r.Body = http.MaxBytesReader(w, r.Body, maxDownloadsRequestBodyBytes)
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
return
}
// Cap batch size
downloads := req.Downloads
if len(downloads) > 100 {
downloads = downloads[:100]
}
ip := clientIP(r)
ipHash := telemetry.HashIP(ip)
uaHash := telemetry.HashUserAgent(r.Header.Get("User-Agent"))
dedupeWindow := a.Config.Telemetry.DedupeWindowSeconds
for _, dl := range downloads {
if dl.Name == "" || dl.Version == "" {
continue
}
pkgID, err := telemetry.LookupPackageID(r.Context(), a.DB, dl.Name)
if err != nil || pkgID == 0 {
continue
}
_, _ = telemetry.RecordInstall(r.Context(), a.DB, telemetry.InstallParams{
PackageID: pkgID,
Version: dl.Version,
IPHash: ipHash,
UserAgentHash: uaHash,
}, dedupeWindow)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
}
// clientIP extracts the IP address from RemoteAddr, stripping the port.
func clientIP(r *http.Request) string {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr // fallback if no port
}
return host
}