// Copyright (C) 2026 Dcntrlized // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package catalog import ( "io" "net/http" "os" "strconv" "git.dcntrlized.org/Dcntrlized/Maestro/lib/catalog" "git.dcntrlized.org/Dcntrlized/Maestro/lib/env" "git.dcntrlized.org/Dcntrlized/Maestro/lib/logger" ) // Download serves GET /api/catalog/artifacts/{id}/{filename} — the public, // session-free artifact surface. Hosted entries stream exact bytes with their // integrity headers; link entries 302 to the operator's URL. Unknown entries, // a mismatched filename and DCNTRLIZED_PUBLIC_PROFILE=false all answer 404. func (h *Handler) Download(w http.ResponseWriter, r *http.Request) { if !env.PublicProfile() { writeError(w, http.StatusNotFound, "ERR_NOT_FOUND", "catalog is not public") return } id := r.PathValue("id") filename := r.PathValue("filename") app, err := h.store.Get(id) if err != nil { logger.Error("catalog: get %s: %v", id, err) writeError(w, http.StatusInternalServerError, "ERR_INTERNAL", "failed to read catalog") return } if app == nil || app.Artifact.Filename == "" || catalog.SafeFilename(filename) != app.Artifact.Filename { writeError(w, http.StatusNotFound, "ERR_NOT_FOUND", "unknown artifact") return } if app.Artifact.Kind == catalog.KindLink { if app.Artifact.SourceURL == "" { writeError(w, http.StatusNotFound, "ERR_NOT_FOUND", "unknown artifact") return } w.Header().Set("Access-Control-Allow-Origin", "*") http.Redirect(w, r, app.Artifact.SourceURL, http.StatusFound) return } path, err := h.store.ResolveArtifact(*app) if err != nil { writeError(w, http.StatusNotFound, "ERR_NOT_FOUND", "unknown artifact") return } file, err := os.Open(path) if err != nil { writeError(w, http.StatusNotFound, "ERR_NOT_FOUND", "unknown artifact") return } defer file.Close() info, err := file.Stat() if err != nil { writeError(w, http.StatusNotFound, "ERR_NOT_FOUND", "unknown artifact") return } contentType := app.Artifact.ContentType if contentType == "" { contentType = catalog.DefaultArtifactContentType } w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Length", strconv.FormatInt(info.Size(), 10)) w.Header().Set("X-Content-SHA256", app.Artifact.SHA256) w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(http.StatusOK) if _, err := io.Copy(w, file); err != nil { logger.Warn("catalog: stream artifact %s: %v", id, err) } } // IconBytes serves GET /api/catalog/icons/{id} — uploaded icon bytes, or 404 // when there is no uploaded icon / the catalog is private. func (h *Handler) IconBytes(w http.ResponseWriter, r *http.Request) { if !env.PublicProfile() { writeError(w, http.StatusNotFound, "ERR_NOT_FOUND", "catalog is not public") return } id := r.PathValue("id") app, err := h.store.Get(id) if err != nil { logger.Error("catalog: get %s: %v", id, err) writeError(w, http.StatusInternalServerError, "ERR_INTERNAL", "failed to read catalog") return } if app == nil || app.IconPath == "" { writeError(w, http.StatusNotFound, "ERR_NOT_FOUND", "unknown icon") return } path, err := h.store.ResolveIcon(*app) if err != nil { writeError(w, http.StatusNotFound, "ERR_NOT_FOUND", "unknown icon") return } file, err := os.Open(path) if err != nil { writeError(w, http.StatusNotFound, "ERR_NOT_FOUND", "unknown icon") return } defer file.Close() info, err := file.Stat() if err != nil { writeError(w, http.StatusNotFound, "ERR_NOT_FOUND", "unknown icon") return } head := make([]byte, 512) n, _ := file.Read(head) contentType := http.DetectContentType(head[:n]) if contentType == "" { contentType = "application/octet-stream" } // Rewind: Read advanced the file offset. if _, err := file.Seek(0, io.SeekStart); err != nil { writeError(w, http.StatusInternalServerError, "ERR_INTERNAL", "failed to read icon") return } w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Length", strconv.FormatInt(info.Size(), 10)) w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(http.StatusOK) if _, err := io.Copy(w, file); err != nil { logger.Warn("catalog: stream icon %s: %v", id, err) } }