165 lines
4.7 KiB
Go
165 lines
4.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/mattnite/cairn/internal/blob"
|
|
"github.com/mattnite/cairn/internal/fingerprint"
|
|
"github.com/mattnite/cairn/internal/forgejo"
|
|
"github.com/mattnite/cairn/internal/models"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type IngestHandler struct {
|
|
DB *gorm.DB
|
|
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.DB, req.Owner, req.Repository)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
commit := &models.Commit{RepositoryID: repo.ID, SHA: req.CommitSHA}
|
|
if err := h.DB.WithContext(ctx).Where("repository_id = ? AND sha = ?", repo.ID, req.CommitSHA).FirstOrCreate(commit).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
shortSHA := commit.SHA
|
|
if len(shortSHA) > 8 {
|
|
shortSHA = shortSHA[:8]
|
|
}
|
|
blobKey := fmt.Sprintf("%s/%s/%s/%s", repo.Name, shortSHA, 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.DB, 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.DB, repo.ID, result.Fingerprint, stackTrace)
|
|
if err == nil {
|
|
_ = h.DB.WithContext(ctx).
|
|
Model(&models.Artifact{}).
|
|
Where("id = ?", artifact.ID).
|
|
Update("crash_signature_id", sig.ID).Error
|
|
|
|
if isNew {
|
|
title := req.Type + " crash in " + req.Repository
|
|
if len(result.Frames) > 0 && strings.TrimSpace(result.Frames[0].Function) != "" {
|
|
title = req.Type + ": " + result.Frames[0].Function
|
|
}
|
|
group, groupErr := models.CreateCrashGroup(ctx, h.DB, 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 {
|
|
DB *gorm.DB
|
|
Store blob.Store
|
|
}
|
|
|
|
func (h *DownloadHandler) Download(c *gin.Context) {
|
|
id, err := parseUintID(c.Param("id"), "artifact id")
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
artifact, err := models.GetArtifact(c.Request.Context(), h.DB, 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)
|
|
}
|