cairn/internal/handler/ingest.go

153 lines
4.3 KiB
Go

package handler
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/mattnite/cairn/internal/blob"
"github.com/mattnite/cairn/internal/fingerprint"
"github.com/mattnite/cairn/internal/forgejo"
"github.com/mattnite/cairn/internal/models"
)
type IngestHandler struct {
Pool *pgxpool.Pool
Store blob.Store
ForgejoSync *forgejo.Sync
}
type IngestRequest struct {
Repository string `json:"repository"`
Owner string `json:"owner"`
CommitSHA string `json:"commit_sha"`
Type string `json:"type"`
CrashMessage string `json:"crash_message,omitempty"`
StackTrace string `json:"stack_trace,omitempty"`
Tags json.RawMessage `json:"tags,omitempty"`
Metadata json.RawMessage `json:"metadata,omitempty"`
}
func (h *IngestHandler) Create(c *gin.Context) {
metaJSON := c.PostForm("meta")
if metaJSON == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "missing 'meta' form field"})
return
}
var req IngestRequest
if err := json.Unmarshal([]byte(metaJSON), &req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid meta JSON: " + err.Error()})
return
}
if req.Repository == "" || req.Owner == "" || req.CommitSHA == "" || req.Type == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "repository, owner, commit_sha, and type are required"})
return
}
file, header, err := c.Request.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "missing 'file' form field: " + err.Error()})
return
}
defer file.Close()
ctx := c.Request.Context()
repo, err := models.GetOrCreateRepository(ctx, h.Pool, req.Owner, req.Repository)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
commit, err := models.GetOrCreateCommit(ctx, h.Pool, repo.ID, req.CommitSHA)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
blobKey := fmt.Sprintf("%s/%s/%s/%s", repo.Name, commit.SHA[:8], req.Type, header.Filename)
if err := h.Store.Put(ctx, blobKey, file, header.Size); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "storing blob: " + err.Error()})
return
}
var crashMsg, stackTrace *string
if req.CrashMessage != "" {
crashMsg = &req.CrashMessage
}
if req.StackTrace != "" {
stackTrace = &req.StackTrace
}
artifact, err := models.CreateArtifact(ctx, h.Pool, models.CreateArtifactParams{
RepositoryID: repo.ID,
CommitID: commit.ID,
Type: req.Type,
BlobKey: blobKey,
BlobSize: header.Size,
CrashMessage: crashMsg,
StackTrace: stackTrace,
Tags: req.Tags,
Metadata: req.Metadata,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Run fingerprinting pipeline if we have a stack trace.
if req.StackTrace != "" {
if result := fingerprint.Compute(req.StackTrace); result != nil {
sig, isNew, err := models.GetOrCreateSignature(ctx, h.Pool, repo.ID, result.Fingerprint, stackTrace)
if err == nil {
_ = models.UpdateArtifactSignature(ctx, h.Pool, artifact.ID, sig.ID, result.Fingerprint)
if isNew {
title := req.Type + " crash in " + req.Repository
if len(result.Frames) > 0 {
title = req.Type + ": " + result.Frames[0].Function
}
group, groupErr := models.CreateCrashGroup(ctx, h.Pool, sig.ID, repo.ID, title)
if groupErr == nil && h.ForgejoSync != nil {
_ = h.ForgejoSync.CreateIssueForCrashGroup(ctx, group, req.StackTrace)
}
}
}
}
}
c.JSON(http.StatusCreated, artifact)
}
type DownloadHandler struct {
Pool *pgxpool.Pool
Store blob.Store
}
func (h *DownloadHandler) Download(c *gin.Context) {
id := c.Param("id")
artifact, err := models.GetArtifact(c.Request.Context(), h.Pool, id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "artifact not found"})
return
}
reader, err := h.Store.Get(c.Request.Context(), artifact.BlobKey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "reading blob: " + err.Error()})
return
}
defer reader.Close()
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", artifact.BlobKey))
c.Header("Content-Type", "application/octet-stream")
_, _ = io.Copy(c.Writer, reader)
}