cairn/internal/handler/targets.go

377 lines
9.0 KiB
Go

package handler
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"net/http"
"path/filepath"
"strconv"
"github.com/gin-gonic/gin"
cairnapi "github.com/mattnite/cairn/internal/api"
"github.com/mattnite/cairn/internal/blob"
"github.com/mattnite/cairn/internal/models"
"gorm.io/gorm"
)
type TargetHandler struct {
DB *gorm.DB
Store blob.Store
}
func (h *TargetHandler) List(c *gin.Context) {
limit, _ := strconv.Atoi(c.Query("limit"))
offset, _ := strconv.Atoi(c.Query("offset"))
if limit <= 0 {
limit = 50
}
repoID, err := parseOptionalUintID(c.Query("repository_id"), "repository_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
targets, total, err := models.ListTargets(c.Request.Context(), h.DB, repoID, limit, offset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if targets == nil {
targets = []cairnapi.Target{}
}
c.JSON(http.StatusOK, gin.H{
"targets": targets,
"total": total,
"limit": limit,
"offset": offset,
})
}
func (h *TargetHandler) Detail(c *gin.Context) {
id, err := parseUintID(c.Param("id"), "target id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
target, err := models.GetTarget(c.Request.Context(), h.DB, id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "target not found"})
return
}
c.JSON(http.StatusOK, target)
}
type EnsureTargetRequest struct {
Repository string `json:"repository" binding:"required"`
Owner string `json:"owner" binding:"required"`
Name string `json:"name" binding:"required"`
Type string `json:"type" binding:"required"`
}
func (h *TargetHandler) Ensure(c *gin.Context) {
var req EnsureTargetRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
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
}
target, err := models.GetOrCreateTarget(ctx, h.DB, models.CreateTargetParams{
RepositoryID: repo.ID,
Name: req.Name,
Type: req.Type,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, target)
}
// Run handlers
type RunHandler struct {
DB *gorm.DB
}
type StartRunRequest struct {
TargetID uint `json:"target_id" binding:"required"`
CommitSHA string `json:"commit_sha" binding:"required"`
}
func (h *RunHandler) Start(c *gin.Context) {
var req StartRunRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx := c.Request.Context()
// Look up the target to get the repository ID for the commit.
target, err := models.GetTarget(ctx, h.DB, req.TargetID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "target not found"})
return
}
commit := &models.Commit{RepositoryID: target.RepositoryID, SHA: req.CommitSHA}
if err := h.DB.WithContext(ctx).Where("repository_id = ? AND sha = ?", target.RepositoryID, req.CommitSHA).FirstOrCreate(commit).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
run, err := models.CreateRun(ctx, h.DB, req.TargetID, commit.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, run)
}
func (h *RunHandler) Finish(c *gin.Context) {
id, err := parseUintID(c.Param("id"), "run id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := models.FinishRun(c.Request.Context(), h.DB, id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "finished"})
}
func (h *RunHandler) Detail(c *gin.Context) {
id, err := parseUintID(c.Param("id"), "run id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
run, err := models.GetRun(c.Request.Context(), h.DB, id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "run not found"})
return
}
c.JSON(http.StatusOK, run)
}
func (h *RunHandler) List(c *gin.Context) {
limit, _ := strconv.Atoi(c.Query("limit"))
offset, _ := strconv.Atoi(c.Query("offset"))
if limit <= 0 {
limit = 50
}
targetID, err := parseOptionalUintID(c.Query("target_id"), "target_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
runs, total, err := models.ListRuns(c.Request.Context(), h.DB, targetID, limit, offset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if runs == nil {
runs = []cairnapi.Run{}
}
c.JSON(http.StatusOK, gin.H{
"runs": runs,
"total": total,
"limit": limit,
"offset": offset,
})
}
// Corpus handlers
type CorpusHandler struct {
DB *gorm.DB
Store blob.Store
}
func (h *CorpusHandler) Upload(c *gin.Context) {
targetID, err := parseUintID(c.Param("id"), "target id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx := c.Request.Context()
// Verify target exists.
target, err := models.GetTarget(ctx, h.DB, targetID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "target not found"})
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()
var runID *uint
if rid := c.PostForm("run_id"); rid != "" {
id, err := strconv.ParseUint(rid, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid run_id"})
return
}
uid := uint(id)
runID = &uid
}
blobKey := fmt.Sprintf("corpus/%s/%s/%s", target.RepoName, target.Name, 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
}
entry, err := models.CreateCorpusEntry(ctx, h.DB, models.CreateCorpusEntryParams{
TargetID: targetID,
RunID: runID,
BlobKey: blobKey,
BlobSize: header.Size,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, entry)
}
func (h *CorpusHandler) List(c *gin.Context) {
targetID, err := parseUintID(c.Param("id"), "target id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
limit, _ := strconv.Atoi(c.Query("limit"))
offset, _ := strconv.Atoi(c.Query("offset"))
entries, total, err := models.ListCorpusEntries(c.Request.Context(), h.DB, targetID, limit, offset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if entries == nil {
entries = []cairnapi.CorpusEntry{}
}
c.JSON(http.StatusOK, gin.H{
"entries": entries,
"total": total,
"limit": limit,
"offset": offset,
})
}
func (h *CorpusHandler) Download(c *gin.Context) {
entryID, err := parseUintID(c.Param("entry_id"), "corpus entry id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx := c.Request.Context()
entry := &models.CorpusEntry{}
if err := h.DB.WithContext(ctx).First(entry, entryID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "corpus entry not found"})
return
}
reader, err := h.Store.Get(ctx, entry.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", entry.BlobKey))
c.Header("Content-Type", "application/octet-stream")
_, _ = io.Copy(c.Writer, reader)
}
func (h *CorpusHandler) DownloadAll(c *gin.Context) {
targetID, err := parseUintID(c.Param("id"), "target id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx := c.Request.Context()
var entries []models.CorpusEntry
if err := h.DB.WithContext(ctx).Where("target_id = ?", targetID).Find(&entries).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if len(entries) == 0 {
c.Status(http.StatusNoContent)
return
}
c.Header("Content-Type", "application/gzip")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"corpus-%d.tar.gz\"", targetID))
gw := gzip.NewWriter(c.Writer)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
for _, entry := range entries {
reader, err := h.Store.Get(ctx, entry.BlobKey)
if err != nil {
continue
}
data, err := io.ReadAll(reader)
reader.Close()
if err != nil {
continue
}
hdr := &tar.Header{
Name: filepath.Base(entry.BlobKey),
Mode: 0o644,
Size: int64(len(data)),
}
if err := tw.WriteHeader(hdr); err != nil {
return
}
if _, err := tw.Write(data); err != nil {
return
}
}
}