187
internal/handler/build.go
Normal file
187
internal/handler/build.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"context"
|
||||
|
||||
"ci/internal/model"
|
||||
"ci/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// BuildHandler handles build/run/log endpoints.
|
||||
type BuildHandler struct {
|
||||
Svc *service.ProjectService
|
||||
}
|
||||
|
||||
func NewBuildHandler(svc *service.ProjectService) *BuildHandler {
|
||||
return &BuildHandler{Svc: svc}
|
||||
}
|
||||
|
||||
// TriggerBuild starts a build for the project.
|
||||
func (h *BuildHandler) TriggerBuild(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
|
||||
return
|
||||
}
|
||||
|
||||
// Build in background
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
h.Svc.BuildProject(ctx, id)
|
||||
}()
|
||||
|
||||
c.JSON(http.StatusAccepted, gin.H{"message": "build started"})
|
||||
}
|
||||
|
||||
// StartProject starts the project's binary.
|
||||
func (h *BuildHandler) StartProject(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := h.Svc.StartProject(ctx, id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "started"})
|
||||
}
|
||||
|
||||
// StopProject stops a running project.
|
||||
func (h *BuildHandler) StopProject(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.Svc.StopProject(id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "stopped"})
|
||||
}
|
||||
|
||||
// RestartProject restarts a project.
|
||||
func (h *BuildHandler) RestartProject(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := h.Svc.RestartProject(ctx, id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "restarted"})
|
||||
}
|
||||
|
||||
// GetBuilds lists build history for a project.
|
||||
func (h *BuildHandler) GetBuilds(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
|
||||
return
|
||||
}
|
||||
|
||||
records, err := h.Svc.GetBuildHistory(id, 50)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if records == nil {
|
||||
records = []model.BuildRecord{}
|
||||
}
|
||||
c.JSON(http.StatusOK, records)
|
||||
}
|
||||
|
||||
// GetBuildLog returns the full log for a specific build.
|
||||
func (h *BuildHandler) GetBuildLog(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid build id"})
|
||||
return
|
||||
}
|
||||
|
||||
logContent, err := h.Svc.GetBuildLog(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, logContent)
|
||||
}
|
||||
|
||||
// StreamLogs streams real-time logs via SSE for a project.
|
||||
func (h *BuildHandler) StreamLogs(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
|
||||
ch := h.Svc.SubscribeLogs(id)
|
||||
defer h.Svc.UnsubscribeLogs(id, ch)
|
||||
|
||||
clientGone := c.Request.Context().Done()
|
||||
|
||||
c.Stream(func(w io.Writer) bool {
|
||||
select {
|
||||
case <-clientGone:
|
||||
return false
|
||||
case line, ok := <-ch:
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
fmt.Fprintf(w, "data: %s\n\n", line)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Webhook handles Git webhook triggers (Gitee/GitHub).
|
||||
func (h *BuildHandler) Webhook(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify project exists
|
||||
_, err = h.Svc.GetProject(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Trigger build
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
h.Svc.BuildProject(ctx, id)
|
||||
}()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "webhook received, build triggered"})
|
||||
}
|
||||
151
internal/handler/deploy.go
Normal file
151
internal/handler/deploy.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ci/internal/model"
|
||||
"ci/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DeployHandler handles deployment endpoints.
|
||||
type DeployHandler struct {
|
||||
Svc *service.ProjectService
|
||||
}
|
||||
|
||||
// NewDeployHandler creates a new DeployHandler.
|
||||
func NewDeployHandler(svc *service.ProjectService) *DeployHandler {
|
||||
return &DeployHandler{Svc: svc}
|
||||
}
|
||||
|
||||
// GetDeployConfig returns the deploy config for a project.
|
||||
func (h *DeployHandler) GetDeployConfig(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := h.Svc.GetDeployConfig(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if cfg == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "no deploy config"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, cfg)
|
||||
}
|
||||
|
||||
// UpdateDeployConfig creates or updates the deploy config for a project.
|
||||
func (h *DeployHandler) UpdateDeployConfig(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
|
||||
return
|
||||
}
|
||||
|
||||
var cfg model.DeployConfig
|
||||
if err := c.ShouldBindJSON(&cfg); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
cfg.ProjectID = id
|
||||
|
||||
if err := h.Svc.UpdateDeployConfig(&cfg); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Re-fetch to return clean state (sensitive fields hidden by json:"-")
|
||||
result, _ := h.Svc.GetDeployConfig(id)
|
||||
if result == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "created"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// DeleteDeployConfig removes the deploy config for a project.
|
||||
func (h *DeployHandler) DeleteDeployConfig(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.Svc.DeleteDeployConfig(id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "deploy config deleted"})
|
||||
}
|
||||
|
||||
// TriggerDeploy starts a deployment for the project.
|
||||
func (h *DeployHandler) TriggerDeploy(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
|
||||
return
|
||||
}
|
||||
|
||||
// Deploy in background
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
h.Svc.TriggerDeploy(ctx, id, nil)
|
||||
}()
|
||||
|
||||
c.JSON(http.StatusAccepted, gin.H{"message": "deployment started"})
|
||||
}
|
||||
|
||||
// GetDeployRecords lists deploy history for a project.
|
||||
func (h *DeployHandler) GetDeployRecords(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
|
||||
return
|
||||
}
|
||||
|
||||
records, err := h.Svc.GetDeployHistory(id, 50)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if records == nil {
|
||||
records = []model.DeployRecord{}
|
||||
}
|
||||
c.JSON(http.StatusOK, records)
|
||||
}
|
||||
|
||||
// GetDeployLog returns the full log for a specific deployment.
|
||||
func (h *DeployHandler) GetDeployLog(c *gin.Context) {
|
||||
didStr := c.Param("did")
|
||||
did, err := parseIDStr(didStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid deploy id"})
|
||||
return
|
||||
}
|
||||
|
||||
logContent, err := h.Svc.GetDeployLog(did)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, logContent)
|
||||
}
|
||||
|
||||
// parseIDStr parses an ID string for non-param-based IDs (e.g., deploy record ID from URL path).
|
||||
func parseIDStr(s string) (uint, error) {
|
||||
id, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint(id), nil
|
||||
}
|
||||
129
internal/handler/project.go
Normal file
129
internal/handler/project.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ci/internal/model"
|
||||
"ci/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ProjectHandler handles project CRUD endpoints.
|
||||
type ProjectHandler struct {
|
||||
Svc *service.ProjectService
|
||||
}
|
||||
|
||||
func NewProjectHandler(svc *service.ProjectService) *ProjectHandler {
|
||||
return &ProjectHandler{Svc: svc}
|
||||
}
|
||||
|
||||
// ListProjects returns all projects.
|
||||
func (h *ProjectHandler) ListProjects(c *gin.Context) {
|
||||
projects, err := h.Svc.ListProjects()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if projects == nil {
|
||||
projects = []model.Project{}
|
||||
}
|
||||
c.JSON(http.StatusOK, projects)
|
||||
}
|
||||
|
||||
// GetProject returns a single project by ID.
|
||||
func (h *ProjectHandler) GetProject(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
|
||||
return
|
||||
}
|
||||
project, err := h.Svc.GetProject(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, project)
|
||||
}
|
||||
|
||||
// CreateProject creates a new project.
|
||||
func (h *ProjectHandler) CreateProject(c *gin.Context) {
|
||||
var p model.Project
|
||||
if err := c.ShouldBindJSON(&p); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if p.Name == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
|
||||
return
|
||||
}
|
||||
if p.Mode != "git" && p.Mode != "upload" {
|
||||
p.Mode = "git" // default
|
||||
}
|
||||
if err := h.Svc.CreateProject(&p); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, p)
|
||||
}
|
||||
|
||||
// UpdateProject updates an existing project.
|
||||
func (h *ProjectHandler) UpdateProject(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
|
||||
return
|
||||
}
|
||||
|
||||
existing, err := h.Svc.GetProject(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var updates model.Project
|
||||
if err := c.ShouldBindJSON(&updates); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Preserve fields that should not be updated from the request
|
||||
updates.ID = existing.ID
|
||||
updates.CreatedAt = existing.CreatedAt
|
||||
updates.Status = existing.Status
|
||||
updates.PID = existing.PID
|
||||
// Preserve password if not sent
|
||||
if updates.GitPassword == "" {
|
||||
updates.GitPassword = existing.GitPassword
|
||||
}
|
||||
|
||||
if err := h.Svc.UpdateProject(&updates); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, updates)
|
||||
}
|
||||
|
||||
// DeleteProject deletes a project.
|
||||
func (h *ProjectHandler) DeleteProject(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
|
||||
return
|
||||
}
|
||||
if err := h.Svc.DeleteProject(id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
|
||||
}
|
||||
|
||||
func parseID(c *gin.Context) (uint, error) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint(id), nil
|
||||
}
|
||||
76
internal/handler/upload.go
Normal file
76
internal/handler/upload.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"ci/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// UploadHandler handles file upload endpoints.
|
||||
type UploadHandler struct {
|
||||
Svc *service.ProjectService
|
||||
}
|
||||
|
||||
func NewUploadHandler(svc *service.ProjectService) *UploadHandler {
|
||||
return &UploadHandler{Svc: svc}
|
||||
}
|
||||
|
||||
// UploadFiles handles source file upload for a project (zip/tar.gz).
|
||||
func (h *UploadHandler) UploadFiles(c *gin.Context) {
|
||||
id, err := parseID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify project exists and is upload mode
|
||||
project, err := h.Svc.GetProject(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no file provided"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate file extension
|
||||
ext := strings.ToLower(filepath.Ext(file.Filename))
|
||||
valid := false
|
||||
if ext == ".zip" {
|
||||
valid = true
|
||||
}
|
||||
if strings.HasSuffix(strings.ToLower(file.Filename), ".tar.gz") || strings.HasSuffix(strings.ToLower(file.Filename), ".tgz") {
|
||||
valid = true
|
||||
}
|
||||
|
||||
if !valid {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "only .zip and .tar.gz files are supported"})
|
||||
return
|
||||
}
|
||||
|
||||
// Save uploaded file to temp location
|
||||
tmpPath := filepath.Join(h.Svc.Workspace.ProjectDir(project.ID), file.Filename)
|
||||
if err := c.SaveUploadedFile(file, tmpPath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to save file: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Extract to src directory
|
||||
if err := h.Svc.Workspace.ExtractArchive(project.ID, tmpPath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to extract: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "file uploaded and extracted successfully",
|
||||
"filename": file.Filename,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user