mirror of
https://github.com/veops/oneterm.git
synced 2025-09-27 03:36:02 +08:00
406 lines
14 KiB
Go
406 lines
14 KiB
Go
package controller
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/spf13/cast"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/veops/oneterm/internal/acl"
|
|
"github.com/veops/oneterm/internal/model"
|
|
"github.com/veops/oneterm/internal/service"
|
|
myErrors "github.com/veops/oneterm/pkg/errors"
|
|
)
|
|
|
|
var storageService = service.DefaultStorageService
|
|
|
|
// ListStorageConfigs godoc
|
|
//
|
|
// @Tags storage
|
|
// @Summary List all storage configurations
|
|
// @Param page_index query int false "page_index"
|
|
// @Param page_size query int false "page_size"
|
|
// @Param search query string false "search"
|
|
// @Param type query string false "storage type filter"
|
|
// @Param enabled query string false "enabled filter (true/false)"
|
|
// @Param primary query string false "primary filter (true/false)"
|
|
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.StorageConfig}}
|
|
// @Router /storage/configs [get]
|
|
func (c *Controller) ListStorageConfigs(ctx *gin.Context) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
if !acl.IsAdmin(currentUser) {
|
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.READ}})
|
|
return
|
|
}
|
|
|
|
db := storageService.BuildQuery(ctx)
|
|
doGet[*model.StorageConfig](ctx, false, db, "")
|
|
}
|
|
|
|
// GetStorageConfig godoc
|
|
//
|
|
// @Tags storage
|
|
// @Summary Get storage configuration by ID
|
|
// @Param id path int true "Storage ID"
|
|
// @Success 200 {object} HttpResponse{data=model.StorageConfig}
|
|
// @Router /storage/configs/{id} [get]
|
|
func (c *Controller) GetStorageConfig(ctx *gin.Context) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
if !acl.IsAdmin(currentUser) {
|
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.READ}})
|
|
return
|
|
}
|
|
|
|
baseService := service.NewBaseService()
|
|
id, err := cast.ToIntE(ctx.Param("id"))
|
|
if err != nil {
|
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
|
|
config := &model.StorageConfig{}
|
|
if err := baseService.GetById(ctx, id, config); err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
ctx.AbortWithError(http.StatusNotFound, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": "storage config not found"}})
|
|
return
|
|
}
|
|
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, HttpResponse{Data: config})
|
|
}
|
|
|
|
// CreateStorageConfig godoc
|
|
//
|
|
// @Tags storage
|
|
// @Summary Create a new storage configuration
|
|
// @Param config body model.StorageConfig true "Storage configuration"
|
|
// @Success 200 {object} HttpResponse{}
|
|
// @Router /storage/configs [post]
|
|
func (c *Controller) CreateStorageConfig(ctx *gin.Context) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
if !acl.IsAdmin(currentUser) {
|
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
|
|
return
|
|
}
|
|
|
|
doCreate(ctx, false, &model.StorageConfig{}, "", func(ctx *gin.Context, config *model.StorageConfig) {
|
|
// Custom validation for storage config
|
|
if err := validateStorageConfig(config); err != nil {
|
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
|
|
// Initialize storage provider after creation
|
|
if provider, err := storageService.CreateProvider(config); err == nil {
|
|
// Test connection
|
|
if err := provider.HealthCheck(ctx); err != nil {
|
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// UpdateStorageConfig godoc
|
|
//
|
|
// @Tags storage
|
|
// @Summary Update an existing storage configuration
|
|
// @Param id path int true "Storage ID"
|
|
// @Param config body model.StorageConfig true "Storage configuration"
|
|
// @Success 200 {object} HttpResponse{}
|
|
// @Router /storage/configs/{id} [put]
|
|
func (c *Controller) UpdateStorageConfig(ctx *gin.Context) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
if !acl.IsAdmin(currentUser) {
|
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
|
|
return
|
|
}
|
|
|
|
doUpdate(ctx, false, &model.StorageConfig{}, "", func(ctx *gin.Context, config *model.StorageConfig) {
|
|
// Custom validation for storage config
|
|
if err := validateStorageConfig(config); err != nil {
|
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
|
|
// Test connection after update
|
|
if provider, err := storageService.CreateProvider(config); err == nil {
|
|
if err := provider.HealthCheck(ctx); err != nil {
|
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
}
|
|
})
|
|
|
|
// Always refresh providers after update to handle name changes
|
|
if !ctx.IsAborted() {
|
|
if err := storageService.RefreshProviders(ctx); err != nil {
|
|
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// DeleteStorageConfig godoc
|
|
//
|
|
// @Tags storage
|
|
// @Summary Delete a storage configuration
|
|
// @Param id path int true "Storage ID"
|
|
// @Success 200 {object} HttpResponse{}
|
|
// @Router /storage/configs/{id} [delete]
|
|
func (c *Controller) DeleteStorageConfig(ctx *gin.Context) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
if !acl.IsAdmin(currentUser) {
|
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
|
|
return
|
|
}
|
|
|
|
doDelete(ctx, false, &model.StorageConfig{}, "", func(ctx *gin.Context, id int) {
|
|
// Custom validation: check if it's the primary storage
|
|
config := &model.StorageConfig{}
|
|
baseService := service.NewBaseService()
|
|
if err := baseService.GetById(ctx, id, config); err == nil && config.IsPrimary {
|
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": "cannot delete primary storage"}})
|
|
return
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestStorageConnection godoc
|
|
//
|
|
// @Tags storage
|
|
// @Summary Test storage connection
|
|
// @Param config body model.StorageConfig true "Storage configuration to test"
|
|
// @Success 200 {object} HttpResponse{}
|
|
// @Router /storage/test-connection [post]
|
|
func (c *Controller) TestStorageConnection(ctx *gin.Context) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
if !acl.IsAdmin(currentUser) {
|
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
|
|
return
|
|
}
|
|
|
|
config := &model.StorageConfig{}
|
|
if err := ctx.ShouldBindBodyWithJSON(config); err != nil {
|
|
// Provide more detailed error information for JSON parsing issues
|
|
errorMsg := fmt.Sprintf("failed to parse request body: %v", err)
|
|
if err.Error() == "EOF" {
|
|
errorMsg = "request body is empty, please provide storage configuration JSON"
|
|
}
|
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": errorMsg}})
|
|
return
|
|
}
|
|
|
|
// Validate required fields
|
|
if config.Type == "" {
|
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": "storage type is required"}})
|
|
return
|
|
}
|
|
|
|
// Create a temporary provider to test connection
|
|
provider, err := storageService.CreateProvider(config)
|
|
if err != nil {
|
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
|
|
// Perform health check
|
|
if err := provider.HealthCheck(ctx); err != nil {
|
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
|
}
|
|
|
|
// GetStorageHealth godoc
|
|
//
|
|
// @Tags storage
|
|
// @Summary Get health status of all storage providers
|
|
// @Success 200 {object} HttpResponse{data=map[string]any}
|
|
// @Router /storage/health [get]
|
|
func (c *Controller) GetStorageHealth(ctx *gin.Context) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
if !acl.IsAdmin(currentUser) {
|
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.READ}})
|
|
return
|
|
}
|
|
|
|
healthResults := storageService.HealthCheck(ctx)
|
|
|
|
// Convert error map to a more API-friendly format
|
|
healthStatus := make(map[string]map[string]interface{})
|
|
for name, err := range healthResults {
|
|
var errorMsg interface{}
|
|
if err != nil {
|
|
errorMsg = err.Error() // Convert error to string for proper JSON serialization
|
|
} else {
|
|
errorMsg = nil
|
|
}
|
|
|
|
healthStatus[name] = map[string]interface{}{
|
|
"healthy": err == nil,
|
|
"error": errorMsg,
|
|
}
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, HttpResponse{Data: healthStatus})
|
|
}
|
|
|
|
// SetPrimaryStorage godoc
|
|
//
|
|
// @Tags storage
|
|
// @Summary Set a storage provider as primary
|
|
// @Param id path int true "Storage ID"
|
|
// @Success 200 {object} HttpResponse{}
|
|
// @Router /storage/configs/{id}/set-primary [put]
|
|
func (c *Controller) SetPrimaryStorage(ctx *gin.Context) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
if !acl.IsAdmin(currentUser) {
|
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
|
|
return
|
|
}
|
|
|
|
id, err := cast.ToIntE(ctx.Param("id"))
|
|
if err != nil {
|
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
|
|
// Get current config
|
|
baseService := service.NewBaseService()
|
|
config := &model.StorageConfig{}
|
|
if err := baseService.GetById(ctx, id, config); err != nil {
|
|
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
|
|
// First, clear all existing primary flags
|
|
if err := storageService.ClearAllPrimaryFlags(ctx); err != nil {
|
|
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
|
|
// Update to set as primary
|
|
config.IsPrimary = true
|
|
config.UpdaterId = currentUser.GetUid()
|
|
|
|
if err := storageService.UpdateStorageConfig(ctx, config); err != nil {
|
|
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
|
|
// Refresh providers after setting new primary to ensure session replay adapter uses the new primary provider
|
|
if err := storageService.RefreshProviders(ctx); err != nil {
|
|
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
|
}
|
|
|
|
// ToggleStorageProvider godoc
|
|
//
|
|
// @Tags storage
|
|
// @Summary Enable or disable a storage provider
|
|
// @Param id path int true "Storage ID"
|
|
// @Success 200 {object} HttpResponse{}
|
|
// @Router /storage/configs/{id}/toggle [put]
|
|
func (c *Controller) ToggleStorageProvider(ctx *gin.Context) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
if !acl.IsAdmin(currentUser) {
|
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
|
|
return
|
|
}
|
|
|
|
id, err := cast.ToIntE(ctx.Param("id"))
|
|
if err != nil {
|
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
|
|
// Get current config
|
|
baseService := service.NewBaseService()
|
|
config := &model.StorageConfig{}
|
|
if err := baseService.GetById(ctx, id, config); err != nil {
|
|
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
|
|
// Toggle enabled status
|
|
config.Enabled = !config.Enabled
|
|
config.UpdaterId = currentUser.GetUid()
|
|
|
|
if err := storageService.UpdateStorageConfig(ctx, config); err != nil {
|
|
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
|
|
// Refresh providers after toggle to ensure correct provider state
|
|
if err := storageService.RefreshProviders(ctx); err != nil {
|
|
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, HttpResponse{Data: map[string]bool{"enabled": config.Enabled}})
|
|
}
|
|
|
|
// GetStorageMetrics godoc
|
|
//
|
|
// @Tags storage
|
|
// @Summary Get storage usage metrics
|
|
// @Success 200 {object} HttpResponse{data=map[string]any}
|
|
// @Router /storage/metrics [get]
|
|
func (c *Controller) GetStorageMetrics(ctx *gin.Context) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
if !acl.IsAdmin(currentUser) {
|
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.READ}})
|
|
return
|
|
}
|
|
|
|
metrics, err := storageService.GetStorageMetrics(ctx)
|
|
if err != nil {
|
|
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, HttpResponse{Data: metrics})
|
|
}
|
|
|
|
// RefreshStorageMetrics godoc
|
|
//
|
|
// @Tags storage
|
|
// @Summary Refresh storage usage metrics
|
|
// @Success 200 {object} HttpResponse{}
|
|
// @Router /storage/metrics/refresh [post]
|
|
func (c *Controller) RefreshStorageMetrics(ctx *gin.Context) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
if !acl.IsAdmin(currentUser) {
|
|
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
|
|
return
|
|
}
|
|
|
|
if err := storageService.RefreshStorageMetrics(ctx); err != nil {
|
|
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
|
}
|
|
|
|
// validateStorageConfig validates storage configuration
|
|
func validateStorageConfig(config *model.StorageConfig) error {
|
|
if config.Name == "" {
|
|
return &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": "storage name is required"}}
|
|
}
|
|
if config.Type == "" {
|
|
return &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": "storage type is required"}}
|
|
}
|
|
return nil
|
|
}
|