Files
devstar/routers/web/user/setting/appstore.go
2025-08-29 15:09:09 +08:00

363 lines
10 KiB
Go

// Copyright 2024 The Devstar Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"io"
"net/http"
"strings"
appstore_model "code.gitea.io/gitea/models/appstore"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/appstore"
"code.gitea.io/gitea/services/context"
)
const (
tplSettingsAppStore templates.TplName = "user/settings/appstore"
)
// AppStore displays the app store page
func AppStore(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.appstore")
ctx.Data["PageIsSettingsAppStore"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
ctx.HTML(http.StatusOK, tplSettingsAppStore)
}
// AppStoreAPI handles API requests for app store data
func AppStoreAPI(ctx *context.Context) {
action := ctx.PathParam("action")
switch action {
case "apps":
handleGetApps(ctx)
case "categories":
handleGetCategories(ctx)
case "tags":
handleGetTags(ctx)
default:
ctx.JSON(http.StatusBadRequest, map[string]interface{}{
"error": "Invalid action",
})
}
}
// handleGetApps returns the list of apps from database
func handleGetApps(ctx *context.Context) {
// Get query parameters
category := ctx.FormString("category")
tag := ctx.FormString("tag")
search := ctx.FormString("search")
deployment := ctx.FormString("deployment") // docker, kubernetes, all
source := ctx.FormString("source") // local | devstar
manager := appstore.NewManager(ctx)
var apps []appstore.App
var err error
if source == "devstar" {
apps, err = manager.ListAppsFromDevstar()
if err == nil {
// 与本地一致的过滤逻辑(先拿全量,再在服务端筛选)
if category != "" || tag != "" || search != "" || deployment != "" {
var filtered []appstore.App
for _, a := range apps {
if category != "" && a.Category != category {
continue
}
if tag != "" {
matched := false
for _, t := range a.Tags {
if t == tag {
matched = true
break
}
}
if !matched {
continue
}
}
if deployment != "" && deployment != "all" {
// 处理部署类型过滤,包括 'both' 类型
appDeployment := a.DeploymentType
if appDeployment == "" {
// 如果没有 deployment_type 字段,尝试从其他字段获取
if a.Deploy.Type != "" {
appDeployment = a.Deploy.Type
} else {
appDeployment = "docker" // 默认值
}
}
if appDeployment != "both" && appDeployment != deployment {
continue
}
}
if search != "" {
low := strings.ToLower(search)
nameOk := strings.Contains(strings.ToLower(a.Name), low)
descOk := strings.Contains(strings.ToLower(a.Description), low)
authorOk := strings.Contains(strings.ToLower(a.Author), low)
if !(nameOk || descOk || authorOk) {
continue
}
}
filtered = append(filtered, a)
}
apps = filtered
}
}
} else {
if search != "" {
// Convert tag parameter to tags slice
var tags []string
if tag != "" {
tags = strings.Split(tag, ",")
}
apps, err = manager.SearchApps(search, category, tags)
} else {
apps, err = manager.ListApps()
// Filter by category, tag and deployment if specified
if category != "" || tag != "" || deployment != "" {
var filteredApps []appstore.App
for _, app := range apps {
matchCategory := category == "" || app.Category == category
matchTag := tag == ""
if tag != "" {
for _, appTag := range app.Tags {
if appTag == tag {
matchTag = true
break
}
}
}
// 处理部署类型过滤,包括 'both' 类型
matchDeployment := deployment == "" || deployment == "all"
if deployment != "" && deployment != "all" {
appDeployment := app.DeploymentType
if appDeployment == "" {
// 如果没有 deployment_type 字段,尝试从其他字段获取
if app.Deploy.Type != "" {
appDeployment = app.Deploy.Type
} else {
appDeployment = "docker" // 默认值
}
}
matchDeployment = appDeployment == "both" || appDeployment == deployment
}
if matchCategory && matchTag && matchDeployment {
filteredApps = append(filteredApps, app)
}
}
apps = filteredApps
}
}
}
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"error": err.Error(),
})
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"apps": apps,
})
}
// handleGetCategories returns the list of categories
func handleGetCategories(ctx *context.Context) {
categories, err := appstore_model.GetCategories(ctx)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"error": err.Error(),
})
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"categories": categories,
})
}
// handleGetTags returns the list of tags
func handleGetTags(ctx *context.Context) {
tags, err := appstore_model.GetTags(ctx)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"error": err.Error(),
})
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"tags": tags,
})
}
// AppStoreInstall handles app installation
func AppStoreInstall(ctx *context.Context) {
if ctx.Req.Method != "POST" {
ctx.JSON(405, map[string]string{"error": "Method Not Allowed"})
return
}
// 解析表单数据
appID := ctx.FormString("app_id")
configJSON := ctx.FormString("config")
installTarget := ctx.FormString("install_target")
kubeconfig := ctx.FormString("kubeconfig")
kubeconfigContext := ctx.FormString("kubeconfig_context")
if appID == "" {
ctx.JSON(400, map[string]string{"error": "应用ID不能为空"})
return
}
// 权限校验:仅管理员可选择默认位置
if installTarget == "local" && (ctx.Doer == nil || !ctx.Doer.IsAdmin) {
ctx.JSON(403, map[string]string{"error": "仅管理员可选择默认位置"})
return
}
// 创建 manager 并执行安装
manager := appstore.NewManager(ctx)
if err := manager.InstallApp(appID, configJSON, installTarget, kubeconfig, kubeconfigContext); err != nil {
// 根据错误类型返回相应的状态码和消息
if appErr, ok := err.(*appstore.AppStoreError); ok {
switch appErr.Code {
case "APP_NOT_FOUND", "CONFIG_MERGE_ERROR":
ctx.JSON(400, map[string]string{"error": appErr.Message + ": " + appErr.Details})
case "KUBERNETES_INSTALL_ERROR":
ctx.JSON(500, map[string]string{"error": appErr.Message + ": " + appErr.Details})
case "NOT_IMPLEMENTED":
ctx.JSON(501, map[string]string{"error": appErr.Message})
default:
ctx.JSON(500, map[string]string{"error": appErr.Message})
}
} else {
ctx.JSON(500, map[string]string{"error": "安装失败: " + err.Error()})
}
return
}
// 安装成功
if installTarget == "kubeconfig" && kubeconfig != "" {
ctx.Flash.Success("应用已成功安装到自定义位置")
} else {
ctx.Flash.Success("应用已成功安装到默认位置")
}
// 优先根据表单的 redirect_to 回跳(前端会带上当前路径)
if redirectTo := ctx.FormString("redirect_to"); redirectTo != "" {
ctx.RedirectToCurrentSite(redirectTo)
} else {
ctx.Redirect(setting.AppSubURL + "/user/settings/appstore")
}
}
// AppStoreUninstall handles app uninstallation
func AppStoreUninstall(ctx *context.Context) {
if ctx.Req.Method != "POST" {
ctx.JSON(405, map[string]string{"error": "Method Not Allowed"})
return
}
// 解析表单数据
appID := ctx.FormString("app_id")
installTarget := ctx.FormString("install_target")
kubeconfig := ctx.FormString("kubeconfig")
kubeconfigContext := ctx.FormString("kubeconfig_context")
if appID == "" {
ctx.JSON(400, map[string]string{"error": "应用ID不能为空"})
return
}
// 创建 manager 并执行卸载
manager := appstore.NewManager(ctx)
if err := manager.UninstallApp(appID, installTarget, kubeconfig, kubeconfigContext); err != nil {
// 根据错误类型返回相应的状态码和消息
if appErr, ok := err.(*appstore.AppStoreError); ok {
switch appErr.Code {
case "APP_NOT_FOUND":
ctx.JSON(400, map[string]string{"error": appErr.Message + ": " + appErr.Details})
case "KUBERNETES_UNINSTALL_ERROR":
ctx.JSON(500, map[string]string{"error": appErr.Message + ": " + appErr.Details})
case "NOT_IMPLEMENTED":
ctx.JSON(501, map[string]string{"error": appErr.Message})
default:
ctx.JSON(500, map[string]string{"error": appErr.Message})
}
} else {
ctx.JSON(500, map[string]string{"error": "卸载失败: " + err.Error()})
}
return
}
// 卸载成功
if installTarget == "kubeconfig" && kubeconfig != "" {
ctx.Flash.Success("应用已成功从指定位置卸载")
} else {
ctx.Flash.Success("本地卸载功能开发中")
}
// 根据来源页面决定重定向位置
referer := ctx.Req.Header.Get("Referer")
if strings.Contains(referer, "/admin/appstore") {
ctx.Redirect(setting.AppSubURL + "/admin/appstore")
} else {
ctx.Redirect(setting.AppSubURL + "/user/settings/appstore")
}
}
// AppStoreConfigure handles app configuration
func AppStoreConfigure(ctx *context.Context) {
// TODO: Load app configuration form
ctx.Data["Title"] = "App Configuration"
ctx.Data["PageIsSettingsAppStore"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
ctx.HTML(http.StatusOK, tplSettingsAppStore)
}
// AppStoreConfigurePost handles app configuration form submission
func AppStoreConfigurePost(ctx *context.Context) {
// TODO: Handle configuration form submission
ctx.Flash.Success("App configuration feature coming soon")
if redirectTo := ctx.FormString("redirect_to"); redirectTo != "" {
ctx.RedirectToCurrentSite(redirectTo)
} else {
ctx.Redirect(setting.AppSubURL + "/user/settings/appstore")
}
}
// 添加应用API
func AddAppAPI(ctx *context.Context) {
manager := appstore.NewManager(ctx)
if ctx.Req.Method != "POST" {
ctx.JSON(405, map[string]string{"error": "Method Not Allowed"})
return
}
defer ctx.Req.Body.Close()
jsonBytes, err := io.ReadAll(ctx.Req.Body)
if err != nil {
ctx.JSON(400, map[string]string{"error": "读取请求体失败"})
return
}
if err := manager.AddAppFromJSON(jsonBytes); err != nil {
ctx.JSON(400, map[string]string{"error": err.Error()})
return
}
ctx.JSON(200, map[string]string{"msg": "ok"})
}