320 lines
7.8 KiB
Go
320 lines
7.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"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)
|
|
}
|