mirror of
https://github.com/gazer-x/komari.git
synced 2026-06-22 00:05:52 +08:00
refactor: 重写Config配置管理
This commit is contained in:
@@ -101,10 +101,10 @@ jobs:
|
||||
fi
|
||||
VERSION="${{ github.sha }}"
|
||||
VERSION_HASH="${{ github.sha }}"
|
||||
go build -trimpath -ldflags="-s -w -X github.com/komari-monitor/komari/internal/version.CurrentVersion=${VERSION} -X github.com/komari-monitor/komari/internal/version.VersionHash=${VERSION_HASH}" -o $BINARY_NAME
|
||||
go build -trimpath -ldflags="-s -w -X github.com/komari-monitor/komari/internal/conf.Version=${VERSION} -X github.com/komari-monitor/komari/internal/conf.CommitHash=${VERSION_HASH}" -o $BINARY_NAME
|
||||
|
||||
- name: Upload binary as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komari-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: komari-${{ matrix.goos }}-${{ matrix.goarch }}*
|
||||
path: komari-${{ matrix.goos }}-${{ matrix.goarch }}*
|
||||
|
||||
@@ -12,13 +12,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
uses: actions/checkout@v4
|
||||
|
||||
#- name: Create dummy index.html
|
||||
# run: |
|
||||
# mkdir -p public/dist
|
||||
# echo "<html><body><h1>This is komari development server.</h1></body></html>" > public/dist/index.html
|
||||
|
||||
|
||||
- name: Clone and build frontend
|
||||
run: |
|
||||
git clone https://github.com/komari-monitor/komari-web web
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
GOARCH: amd64
|
||||
run: |
|
||||
VERSION_SHA="${{ github.sha }}"
|
||||
go build -ldflags "-X github.com/komari-monitor/komari/internal/version.CurrentVersion=dev -X github.com/komari-monitor/komari/internal/version.VersionHash=${VERSION_SHA}" -o komari .
|
||||
go build -ldflags "-X github.com/komari-monitor/komari/internal/conf.Version=dev -X github.com/komari-monitor/komari/internal/conf.CommitHash=${VERSION_SHA}" -o komari .
|
||||
|
||||
- name: Upload binary as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
run: |
|
||||
VERSION="${{ github.ref_name }}"
|
||||
VERSION_HASH="${{ github.sha }}"
|
||||
LDFLAGS="-s -w -X github.com/komari-monitor/komari/internal/version.CurrentVersion=${VERSION} -X github.com/komari-monitor/komari/internal/version.VersionHash=${VERSION_HASH}"
|
||||
LDFLAGS="-s -w -X github.com/komari-monitor/komari/internal/conf.Version=${VERSION} -X github.com/komari-monitor/komari/internal/conf.CommitHash=${VERSION_HASH}"
|
||||
|
||||
echo "Building for linux/amd64..."
|
||||
GOARCH=amd64 CC="zig cc -target x86_64-linux-musl" go build -trimpath -ldflags="$LDFLAGS" -o komari-linux-amd64
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
run: |
|
||||
VERSION="${{ github.event.release.tag_name }}"
|
||||
VERSION_HASH="${{ github.sha }}"
|
||||
LDFLAGS="-s -w -X github.com/komari-monitor/komari/internal/version.CurrentVersion=${VERSION} -X github.com/komari-monitor/komari/internal/version.VersionHash=${VERSION_HASH}"
|
||||
LDFLAGS="-s -w -X github.com/komari-monitor/komari/internal/conf.Version=${VERSION} -X github.com/komari-monitor/komari/internal/conf.CommitHash=${VERSION_HASH}"
|
||||
|
||||
echo "Building for linux/amd64..."
|
||||
GOARCH=amd64 CC="zig cc -target x86_64-linux-musl" go build -trimpath -ldflags="$LDFLAGS" -o komari-linux-amd64
|
||||
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
fi
|
||||
VERSION="${{ github.event.release.tag_name }}"
|
||||
VERSION_HASH="${{ github.sha }}" # Use commit SHA for hash
|
||||
go build -trimpath -ldflags="-s -w -X github.com/komari-monitor/komari/internal/version.CurrentVersion=${VERSION} -X github.com/komari-monitor/komari/internal/version.VersionHash=${VERSION_HASH}" -o $BINARY_NAME
|
||||
go build -trimpath -ldflags="-s -w -X github.com/komari-monitor/komari/internal/conf.Version=${VERSION} -X github.com/komari-monitor/komari/internal/conf.CommitHash=${VERSION_HASH}" -o $BINARY_NAME
|
||||
|
||||
- name: Upload binary to release
|
||||
env:
|
||||
|
||||
+2
-2
@@ -9,6 +9,6 @@ var (
|
||||
DatabaseUser string // MySQL/其他数据库用户名
|
||||
DatabasePass string // MySQL/其他数据库密码
|
||||
DatabaseName string // MySQL/其他数据库名称
|
||||
|
||||
Listen string
|
||||
ConfigFile string // 配置文件路径
|
||||
Listen string
|
||||
)
|
||||
|
||||
+3
-3
@@ -13,8 +13,8 @@ import (
|
||||
|
||||
apiClient "github.com/komari-monitor/komari/internal/api_v1/client"
|
||||
"github.com/komari-monitor/komari/internal/common"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/auditlog"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/database/dbcore"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"github.com/komari-monitor/komari/internal/geoip"
|
||||
@@ -370,7 +370,7 @@ func (s *nezhaCompatServer) ReportGeoIP(ctx context.Context, in *proto.GeoIP) (*
|
||||
if in != nil && in.Ip != nil {
|
||||
if v4 := strings.TrimSpace(in.Ip.Ipv4); v4 != "" {
|
||||
updates["ipv4"] = v4
|
||||
if cfg, err := config.Get(); err == nil && cfg.GeoIpEnabled {
|
||||
if cfg, err := conf.GetWithV1Format(); err == nil && cfg.GeoIpEnabled {
|
||||
if ip := net.ParseIP(v4); ip != nil {
|
||||
if gi, _ := geoip.GetGeoInfo(ip); gi != nil {
|
||||
iso = gi.ISOCode
|
||||
@@ -381,7 +381,7 @@ func (s *nezhaCompatServer) ReportGeoIP(ctx context.Context, in *proto.GeoIP) (*
|
||||
if v6 := strings.TrimSpace(in.Ip.Ipv6); v6 != "" {
|
||||
updates["ipv6"] = v6
|
||||
if iso == "" { // 优先使用 v4 的国家码
|
||||
if cfg, err := config.Get(); err == nil && cfg.GeoIpEnabled {
|
||||
if cfg, err := conf.GetWithV1Format(); err == nil && cfg.GeoIpEnabled {
|
||||
if ip := net.ParseIP(v6); ip != nil {
|
||||
if gi, _ := geoip.GetGeoInfo(ip); gi != nil {
|
||||
iso = gi.ISOCode
|
||||
|
||||
@@ -3,8 +3,8 @@ package cmd
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/dbcore"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"github.com/spf13/cobra"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -16,7 +16,7 @@ var PermitPasswordLoginCmd = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
db := dbcore.GetDBInstance()
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
return tx.Model(&models.Config{}).Where("id = ?", 1).
|
||||
return tx.Model(&conf.V1Struct{}).Where("id = ?", 1).
|
||||
Update("disable_password_login", false).Error
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
+9
-7
@@ -21,13 +21,14 @@ func GetEnv(key, defaultValue string) string {
|
||||
|
||||
// 从环境变量获取默认值
|
||||
var (
|
||||
dbTypeEnv = GetEnv("KOMARI_DB_TYPE", "sqlite")
|
||||
dbFileEnv = GetEnv("KOMARI_DB_FILE", "./data/komari.db")
|
||||
dbHostEnv = GetEnv("KOMARI_DB_HOST", "localhost")
|
||||
dbPortEnv = GetEnv("KOMARI_DB_PORT", "3306")
|
||||
dbUserEnv = GetEnv("KOMARI_DB_USER", "root")
|
||||
dbPassEnv = GetEnv("KOMARI_DB_PASS", "")
|
||||
dbNameEnv = GetEnv("KOMARI_DB_NAME", "komari")
|
||||
dbTypeEnv = GetEnv("KOMARI_DB_TYPE", "sqlite")
|
||||
dbFileEnv = GetEnv("KOMARI_DB_FILE", "./data/komari.db")
|
||||
dbHostEnv = GetEnv("KOMARI_DB_HOST", "localhost")
|
||||
dbPortEnv = GetEnv("KOMARI_DB_PORT", "3306")
|
||||
dbUserEnv = GetEnv("KOMARI_DB_USER", "root")
|
||||
dbPassEnv = GetEnv("KOMARI_DB_PASS", "")
|
||||
dbNameEnv = GetEnv("KOMARI_DB_NAME", "komari")
|
||||
configFileEnv = GetEnv("KOMARI_CONFIG_FILE", "./data/komari.json")
|
||||
)
|
||||
|
||||
var RootCmd = &cobra.Command{
|
||||
@@ -59,4 +60,5 @@ func init() {
|
||||
RootCmd.PersistentFlags().StringVar(&flags.DatabaseUser, "db-user", dbUserEnv, "MySQL/Other database username [env: KOMARI_DB_USER]")
|
||||
RootCmd.PersistentFlags().StringVar(&flags.DatabasePass, "db-pass", dbPassEnv, "MySQL/Other database password [env: KOMARI_DB_PASS]")
|
||||
RootCmd.PersistentFlags().StringVar(&flags.DatabaseName, "db-name", dbNameEnv, "MySQL/Other database name [env: KOMARI_DB_NAME]")
|
||||
RootCmd.PersistentFlags().StringVarP(&flags.ConfigFile, "config", "c", configFileEnv, "Configuration file path [env: KOMARI_CONFIG_FILE]")
|
||||
}
|
||||
|
||||
+26
-19
@@ -15,10 +15,10 @@ import (
|
||||
"github.com/gookit/event"
|
||||
"github.com/komari-monitor/komari/cmd/flags"
|
||||
api "github.com/komari-monitor/komari/internal/api_v1"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database"
|
||||
"github.com/komari-monitor/komari/internal/database/accounts"
|
||||
"github.com/komari-monitor/komari/internal/database/auditlog"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/database/dbcore"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
d_notification "github.com/komari-monitor/komari/internal/database/notification"
|
||||
@@ -30,7 +30,8 @@ import (
|
||||
"github.com/komari-monitor/komari/internal/messageSender"
|
||||
"github.com/komari-monitor/komari/internal/notifier"
|
||||
"github.com/komari-monitor/komari/internal/oauth"
|
||||
"github.com/komari-monitor/komari/internal/version"
|
||||
"github.com/komari-monitor/komari/internal/patch"
|
||||
"github.com/komari-monitor/komari/internal/restore"
|
||||
"github.com/komari-monitor/komari/pkg/cloudflared"
|
||||
"github.com/komari-monitor/komari/server"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -58,13 +59,19 @@ func RunServer() {
|
||||
if err := os.MkdirAll("./data/theme", os.ModePerm); err != nil {
|
||||
log.Fatalf("Failed to create theme directory: %v", err)
|
||||
}
|
||||
// 进行备份恢复
|
||||
if restore.NeedBackupRestore() {
|
||||
restore.RestoreBackup()
|
||||
}
|
||||
conf.Load()
|
||||
InitDatabase()
|
||||
patch.ApplyPatch()
|
||||
|
||||
if version.VersionHash != "unknown" {
|
||||
if conf.Version != conf.Version_Development {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
conf, err := config.Get()
|
||||
config, err := conf.GetWithV1Format()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -73,21 +80,20 @@ func RunServer() {
|
||||
r.Use(logutil.GinLogger())
|
||||
r.Use(logutil.GinRecovery())
|
||||
|
||||
event.Trigger(eventType.ServerInitializeStart, event.M{"config": conf, "engine": r})
|
||||
defer event.Trigger(eventType.ServerInitializeDone, event.M{"config": conf})
|
||||
event.Trigger(eventType.ServerInitializeStart, event.M{"config": config, "engine": r})
|
||||
|
||||
go geoip.InitGeoIp()
|
||||
go DoScheduledWork()
|
||||
go messageSender.Initialize()
|
||||
go oauth.Initialize()
|
||||
|
||||
server.StartNezhaGRPCServer(conf.NezhaCompatListen)
|
||||
server.StartNezhaGRPCServer(config.NezhaCompatListen)
|
||||
|
||||
event.On(eventType.ConfigUpdated, event.ListenerFunc(func(e event.Event) error {
|
||||
newConf := e.Get("new").(models.Config)
|
||||
oldConf := e.Get("old").(models.Config)
|
||||
if newConf.OAuthProvider != oldConf.OAuthProvider {
|
||||
oidcProvider, err := database.GetOidcConfigByName(newConf.OAuthProvider)
|
||||
newConf := e.Get("new").(conf.Config)
|
||||
oldConf := e.Get("old").(conf.Config)
|
||||
if newConf.Login.OAuthProvider != oldConf.Login.OAuthProvider {
|
||||
oidcProvider, err := database.GetOidcConfigByName(newConf.Login.OAuthProvider)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get OIDC provider config: %v", err)
|
||||
} else {
|
||||
@@ -98,7 +104,7 @@ func RunServer() {
|
||||
auditlog.EventLog("error", fmt.Sprintf("Failed to load OIDC provider: %v", err))
|
||||
}
|
||||
}
|
||||
if newConf.NotificationMethod != oldConf.NotificationMethod {
|
||||
if newConf.Notification.NotificationMethod != oldConf.Notification.NotificationMethod {
|
||||
messageSender.Initialize()
|
||||
}
|
||||
return nil
|
||||
@@ -118,13 +124,14 @@ func RunServer() {
|
||||
Addr: flags.Listen,
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
event.Trigger(eventType.ServerInitializeDone, event.M{"config": config})
|
||||
|
||||
log.Printf("Starting server on %s ...", flags.Listen)
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
OnFatal(err)
|
||||
log.Fatalf("listen: %s\n", err)
|
||||
}
|
||||
}()
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
OnFatal(err)
|
||||
log.Fatalf("listen: %s\n", err)
|
||||
}
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||||
<-quit
|
||||
@@ -168,7 +175,7 @@ func DoScheduledWork() {
|
||||
minute := time.NewTicker(60 * time.Second)
|
||||
//records.DeleteRecordBefore(time.Now().Add(-time.Hour * 24 * 30))
|
||||
records.CompactRecord()
|
||||
cfg, _ := config.Get()
|
||||
cfg, _ := conf.GetWithV1Format()
|
||||
go notifier.CheckExpireScheduledWork()
|
||||
for {
|
||||
select {
|
||||
|
||||
@@ -11,10 +11,9 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/accounts"
|
||||
"github.com/komari-monitor/komari/internal/database/clients"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"github.com/komari-monitor/komari/internal/ws"
|
||||
"github.com/komari-monitor/komari/pkg/rpc"
|
||||
)
|
||||
@@ -26,7 +25,7 @@ func RegisterRouters(path string, r *gin.Engine) {
|
||||
|
||||
// Json Rpc2 over websocket, /api/rpc2
|
||||
func OnRpcRequest(c *gin.Context) {
|
||||
cfg, _ := config.Get()
|
||||
cfg, _ := conf.GetWithV1Format()
|
||||
|
||||
// GET -> WebSocket
|
||||
if c.Request.Method == http.MethodGet {
|
||||
@@ -123,7 +122,7 @@ func OnRpcRequest(c *gin.Context) {
|
||||
}
|
||||
|
||||
// detectPermissionGroup 提取权限分组,与原逻辑保持一致
|
||||
func detectPermissionGroup(c *gin.Context, cfg models.Config) string {
|
||||
func detectPermissionGroup(c *gin.Context, cfg conf.V1Struct) string {
|
||||
permissionGroup := "guest"
|
||||
token := c.Query("Authorization")
|
||||
if _, err := clients.GetClientUUIDByToken(token); err == nil {
|
||||
|
||||
@@ -10,13 +10,12 @@ import (
|
||||
|
||||
api "github.com/komari-monitor/komari/internal/api_v1"
|
||||
"github.com/komari-monitor/komari/internal/common"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database"
|
||||
"github.com/komari-monitor/komari/internal/database/clients"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/database/dbcore"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"github.com/komari-monitor/komari/internal/database/tasks"
|
||||
"github.com/komari-monitor/komari/internal/version"
|
||||
"github.com/komari-monitor/komari/internal/ws"
|
||||
"github.com/komari-monitor/komari/pkg/rpc"
|
||||
|
||||
@@ -229,7 +228,7 @@ func getNodes(ctx context.Context, req *rpc.JsonRpcRequest) (any, *rpc.JsonRpcEr
|
||||
}
|
||||
meta := rpc.MetaFromContext(ctx)
|
||||
|
||||
cfg, _ := config.Get()
|
||||
cfg, _ := conf.GetWithV1Format()
|
||||
if meta.Permission != "admin" {
|
||||
// 过滤 Hidden 节点并隐藏敏感字段
|
||||
filtered := make([]models.Client, 0, len(cinfo))
|
||||
@@ -455,8 +454,8 @@ func getVersion(_ context.Context, _ *rpc.JsonRpcRequest) (any, *rpc.JsonRpcErro
|
||||
Version string `json:"version"`
|
||||
Hash string `json:"hash"`
|
||||
}{
|
||||
Version: version.CurrentVersion,
|
||||
Hash: version.VersionHash,
|
||||
Version: conf.Version,
|
||||
Hash: conf.CommitHash,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -9,12 +9,11 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/komari-monitor/komari/internal/common"
|
||||
"github.com/komari-monitor/komari/internal/version"
|
||||
"github.com/patrickmn/go-cache"
|
||||
|
||||
"strconv"
|
||||
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/dbcore"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"github.com/komari-monitor/komari/pkg/utils"
|
||||
@@ -141,13 +140,13 @@ func RespondError(c *gin.Context, httpStatus int, message string) {
|
||||
}
|
||||
func GetVersion(c *gin.Context) {
|
||||
RespondSuccess(c, gin.H{
|
||||
"version": version.CurrentVersion,
|
||||
"hash": version.VersionHash,
|
||||
"version": conf.Version,
|
||||
"hash": conf.CommitHash,
|
||||
})
|
||||
}
|
||||
|
||||
func isApiKeyValid(apiKey string) bool {
|
||||
cfg, err := config.Get()
|
||||
cfg, err := conf.GetWithV1Format()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package api_v1
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/accounts"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -46,7 +46,7 @@ func PrivateSiteMiddleware() gin.HandlerFunc {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
conf, err := config.Get()
|
||||
conf, err := conf.GetWithV1Format()
|
||||
if err != nil {
|
||||
RespondError(c, http.StatusInternalServerError, "Failed to get configuration.")
|
||||
c.Abort()
|
||||
|
||||
@@ -3,8 +3,8 @@ package admin
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
api "github.com/komari-monitor/komari/internal/api_v1"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"github.com/komari-monitor/komari/internal/messageSender"
|
||||
"github.com/komari-monitor/komari/internal/messageSender/factory"
|
||||
@@ -50,7 +50,7 @@ func SetMessageSenderProvider(c *gin.Context) {
|
||||
api.RespondError(c, 500, "Failed to save message sender provider configuration: "+err.Error())
|
||||
return
|
||||
}
|
||||
cfg, _ := config.Get()
|
||||
cfg, _ := conf.GetWithV1Format()
|
||||
// 正在使用,重载
|
||||
if cfg.NotificationMethod == senderConfig.Name {
|
||||
err := messageSender.LoadProvider(senderConfig.Name, senderConfig.Addition)
|
||||
|
||||
@@ -3,9 +3,9 @@ package admin
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
api "github.com/komari-monitor/komari/internal/api_v1"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database"
|
||||
"github.com/komari-monitor/komari/internal/database/accounts"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"github.com/komari-monitor/komari/internal/oauth"
|
||||
"github.com/komari-monitor/komari/internal/oauth/factory"
|
||||
@@ -80,7 +80,7 @@ func SetOidcProvider(c *gin.Context) {
|
||||
api.RespondError(c, 500, "Failed to save OIDC provider configuration: "+err.Error())
|
||||
return
|
||||
}
|
||||
cfg, _ := config.Get()
|
||||
cfg, _ := conf.GetWithV1Format()
|
||||
// 正在使用,重载
|
||||
if cfg.OAuthProvider == oidcConfig.Name {
|
||||
err := oauth.LoadProvider(oidcConfig.Name, oidcConfig.Addition)
|
||||
|
||||
@@ -4,9 +4,8 @@ import (
|
||||
"database/sql"
|
||||
|
||||
api "github.com/komari-monitor/komari/internal/api_v1"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/auditlog"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"github.com/komari-monitor/komari/internal/database/records"
|
||||
"github.com/komari-monitor/komari/internal/database/tasks"
|
||||
|
||||
@@ -15,13 +14,13 @@ import (
|
||||
|
||||
// GetSettings 获取自定义配置
|
||||
func GetSettings(c *gin.Context) {
|
||||
cst, err := config.Get()
|
||||
cst, err := conf.GetWithV1Format()
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
//override
|
||||
cst = models.Config{Sitename: "Komari"}
|
||||
cst = conf.V1Struct{Sitename: "Komari"}
|
||||
cst.ID = 1
|
||||
config.Save(cst)
|
||||
conf.Save(cst)
|
||||
api.RespondSuccess(c, cst)
|
||||
return
|
||||
}
|
||||
@@ -42,7 +41,7 @@ func EditSettings(c *gin.Context) {
|
||||
}
|
||||
|
||||
cfg["id"] = 1 // Only one record
|
||||
if err := config.Update(cfg); err != nil {
|
||||
if err := conf.Update(cfg); err != nil {
|
||||
api.RespondError(c, 500, "Failed to update settings: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
api "github.com/komari-monitor/komari/internal/api_v1"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"github.com/komari-monitor/komari/internal/geoip"
|
||||
"github.com/komari-monitor/komari/internal/messageSender"
|
||||
@@ -32,7 +32,7 @@ func TestGeoIp(c *gin.Context) {
|
||||
ip = c.ClientIP()
|
||||
}
|
||||
}
|
||||
conf, err := config.Get()
|
||||
conf, err := conf.GetWithV1Format()
|
||||
if err != nil {
|
||||
api.RespondError(c, 500, "Failed to get configuration: "+err.Error())
|
||||
return
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
api "github.com/komari-monitor/komari/internal/api_v1"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/dbcore"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
)
|
||||
@@ -138,7 +138,7 @@ func SetTheme(c *gin.Context) {
|
||||
"theme": themeName,
|
||||
}
|
||||
|
||||
if err := config.Update(updateData); err != nil {
|
||||
if err := conf.Update(updateData); err != nil {
|
||||
api.RespondError(c, http.StatusInternalServerError, "更新主题设置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package client
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
api "github.com/komari-monitor/komari/internal/api_v1"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/clients"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ func RegisterClient(c *gin.Context) {
|
||||
api.RespondError(c, 403, "Invalid AutoDiscovery Key")
|
||||
return
|
||||
}
|
||||
cfg, err := config.Get()
|
||||
cfg, err := conf.GetWithV1Format()
|
||||
if err != nil {
|
||||
api.RespondError(c, 500, "Failed to get configuration: "+err.Error())
|
||||
return
|
||||
|
||||
@@ -3,8 +3,8 @@ package client
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/clients"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/geoip"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -60,7 +60,7 @@ func UploadBasicInfo(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if cfg, err := config.Get(); err == nil && cfg.GeoIpEnabled {
|
||||
if cfg, err := conf.GetWithV1Format(); err == nil && cfg.GeoIpEnabled {
|
||||
if ipv4, ok := cbi["ipv4"].(string); ok && ipv4 != "" {
|
||||
ip4 := net.ParseIP(ipv4)
|
||||
ip4_record, _ := geoip.GetGeoInfo(ip4)
|
||||
|
||||
@@ -5,9 +5,11 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gookit/event"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/accounts"
|
||||
"github.com/komari-monitor/komari/internal/database/auditlog"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/eventType"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -19,7 +21,7 @@ type LoginRequest struct {
|
||||
}
|
||||
|
||||
func Login(c *gin.Context) {
|
||||
conf, _ := config.Get()
|
||||
conf, _ := conf.GetWithV1Format()
|
||||
if conf.DisablePasswordLogin {
|
||||
RespondError(c, http.StatusForbidden, "Password login is disabled")
|
||||
return
|
||||
@@ -44,6 +46,15 @@ func Login(c *gin.Context) {
|
||||
uuid, success := accounts.CheckPassword(data.Username, data.Password)
|
||||
if !success {
|
||||
RespondError(c, http.StatusUnauthorized, "Invalid credentials")
|
||||
event.Trigger(eventType.LoginFailed, event.M{
|
||||
"username": data.Username,
|
||||
"method": "password",
|
||||
"ip": c.ClientIP(),
|
||||
"ua": c.Request.UserAgent(),
|
||||
"header": c.Request.Header,
|
||||
"referrer": c.Request.Referer(),
|
||||
"host": c.Request.Host,
|
||||
})
|
||||
return
|
||||
}
|
||||
// 2FA
|
||||
@@ -67,6 +78,15 @@ func Login(c *gin.Context) {
|
||||
c.SetCookie("session_token", session, 2592000, "/", "", false, true)
|
||||
auditlog.Log(c.ClientIP(), uuid, "logged in (password)", "login")
|
||||
RespondSuccess(c, gin.H{"set-cookie": gin.H{"session_token": session}})
|
||||
event.Trigger(eventType.UserLogin, event.M{
|
||||
"username": data.Username,
|
||||
"method": "password",
|
||||
"ip": c.ClientIP(),
|
||||
"ua": c.Request.UserAgent(),
|
||||
"header": c.Request.Header,
|
||||
"referrer": c.Request.Referer(),
|
||||
"host": c.Request.Host,
|
||||
})
|
||||
}
|
||||
func Logout(c *gin.Context) {
|
||||
session, _ := c.Cookie("session_token")
|
||||
|
||||
@@ -5,16 +5,18 @@ import (
|
||||
"slices"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gookit/event"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/accounts"
|
||||
"github.com/komari-monitor/komari/internal/database/auditlog"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/eventType"
|
||||
"github.com/komari-monitor/komari/internal/oauth"
|
||||
"github.com/komari-monitor/komari/pkg/utils"
|
||||
)
|
||||
|
||||
// /api/oauth
|
||||
func OAuth(c *gin.Context) {
|
||||
cfg, _ := config.Get()
|
||||
cfg, _ := conf.GetWithV1Format()
|
||||
if !cfg.OAuthEnabled {
|
||||
c.JSON(403, gin.H{"status": "error", "error": "OAuth is not enabled"})
|
||||
return
|
||||
@@ -97,6 +99,17 @@ func OAuthCallback(c *gin.Context) {
|
||||
"status": "error",
|
||||
"message": "please log in and bind your external account first.",
|
||||
})
|
||||
event.Trigger(eventType.UserLogin, event.M{
|
||||
"username": user.Username,
|
||||
"method": "oauth",
|
||||
"ip": c.ClientIP(),
|
||||
"ua": c.Request.UserAgent(),
|
||||
"header": c.Request.Header,
|
||||
"referrer": c.Request.Referer(),
|
||||
"host": c.Request.Host,
|
||||
"error": "no linked account",
|
||||
"sso_id": sso_id,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -104,11 +117,32 @@ func OAuthCallback(c *gin.Context) {
|
||||
session, err := accounts.CreateSession(user.UUID, 2592000, c.Request.UserAgent(), c.ClientIP(), "oauth")
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"status": "error", "message": err.Error()})
|
||||
event.Trigger(eventType.LoginFailed, event.M{
|
||||
"username": user.Username,
|
||||
"method": "oauth",
|
||||
"ip": c.ClientIP(),
|
||||
"ua": c.Request.UserAgent(),
|
||||
"header": c.Request.Header,
|
||||
"referrer": c.Request.Referer(),
|
||||
"host": c.Request.Host,
|
||||
"error": err.Error(),
|
||||
"sso_id": sso_id,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置cookie并返回
|
||||
c.SetCookie("session_token", session, 2592000, "/", "", false, true)
|
||||
auditlog.Log(c.ClientIP(), user.UUID, "logged in (OAuth)", "login")
|
||||
event.Trigger(eventType.UserLogin, event.M{
|
||||
"username": user.Username,
|
||||
"method": "oauth",
|
||||
"ip": c.ClientIP(),
|
||||
"ua": c.Request.UserAgent(),
|
||||
"header": c.Request.Header,
|
||||
"referrer": c.Request.Referer(),
|
||||
"host": c.Request.Host,
|
||||
})
|
||||
c.Redirect(302, "/admin")
|
||||
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/komari-monitor/komari/internal/common"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/accounts"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/database/dbcore"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"github.com/komari-monitor/komari/internal/ws"
|
||||
@@ -20,7 +20,7 @@ func GetClients(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "Require WebSocket upgrade"})
|
||||
return
|
||||
}
|
||||
cfg, _ := config.Get()
|
||||
cfg, _ := conf.GetWithV1Format()
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
if cfg.AllowCors {
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/gookit/event"
|
||||
"github.com/komari-monitor/komari/cmd/flags"
|
||||
"github.com/komari-monitor/komari/internal/eventType"
|
||||
)
|
||||
|
||||
func Default() Config {
|
||||
return Config{
|
||||
Site: Site{
|
||||
Sitename: "Komari",
|
||||
Description: "Komari Monitor, a simple server monitoring tool.",
|
||||
AllowCors: false,
|
||||
Theme: "default",
|
||||
},
|
||||
GeoIp: GeoIp{
|
||||
GeoIpEnabled: true,
|
||||
GeoIpProvider: GeoIp_IPInfo,
|
||||
},
|
||||
Notification: Notification{
|
||||
NotificationEnabled: true,
|
||||
TrafficLimitPercentage: 80.00,
|
||||
},
|
||||
Record: Record{
|
||||
RecordEnabled: true,
|
||||
RecordPreserveTime: 720,
|
||||
PingRecordPreserveTime: 24,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func Override(cst Config) error {
|
||||
b, err := json.MarshalIndent(cst, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(flags.ConfigFile, b, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldConf := *Conf
|
||||
Conf = &cst
|
||||
event.Trigger(eventType.ConfigUpdated, event.M{
|
||||
"old": oldConf,
|
||||
"new": cst,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func SavePartial(cst map[string]interface{}) error {
|
||||
// 将当前内存中的配置转换为通用 map,便于合并
|
||||
baseBytes, err := json.Marshal(Conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var base map[string]interface{}
|
||||
if err := json.Unmarshal(baseBytes, &base); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 兼容旧版扁平字段:把扁平键映射到新版分组结构
|
||||
normalized := normalizePartialMap(cst)
|
||||
|
||||
// 深度合并(normalized 覆盖 base)
|
||||
merged := deepMerge(base, normalized)
|
||||
|
||||
// 回写到强类型 Config
|
||||
mergedBytes, err := json.Marshal(merged)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var newConf Config
|
||||
if err := json.Unmarshal(mergedBytes, &newConf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新内存并落盘
|
||||
return Override(newConf)
|
||||
}
|
||||
|
||||
func EditAndTrigger(fn func()) error {
|
||||
oldConf := *Conf
|
||||
fn()
|
||||
event.Trigger(eventType.ConfigUpdated, event.M{
|
||||
"old": oldConf,
|
||||
"new": *Conf,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func SaveFull(cst Config) error {
|
||||
return Override(cst)
|
||||
}
|
||||
|
||||
func Load() (*Config, error) {
|
||||
b, err := os.ReadFile(flags.ConfigFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cst := &Config{}
|
||||
if err := json.Unmarshal(b, cst); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
Conf = cst
|
||||
return cst, nil
|
||||
}
|
||||
|
||||
// GetWithV1Format 以 v1 API 格式获取配置对象,使用 Conf 直接获取对象引用
|
||||
func GetWithV1Format() (V1Struct, error) {
|
||||
return Conf.ToV1Format(), nil
|
||||
}
|
||||
|
||||
func Save(cst V1Struct) error {
|
||||
cfg := cst.ToConfig()
|
||||
return Override(cfg)
|
||||
}
|
||||
|
||||
func Update(cst map[string]interface{}) error {
|
||||
// Update 的语义等同于 SavePartial,保持对旧数据格式兼容
|
||||
return SavePartial(cst)
|
||||
}
|
||||
|
||||
// normalizePartialMap 将可能包含旧版扁平字段的输入映射为新版分组结构。
|
||||
// 若已是分组结构(包含 site/login/...),则原样保留并与映射结果合并。
|
||||
func normalizePartialMap(in map[string]interface{}) map[string]interface{} {
|
||||
if in == nil {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
|
||||
// 先复制一份,避免修改入参
|
||||
out := make(map[string]interface{})
|
||||
for k, v := range in {
|
||||
out[k] = v
|
||||
}
|
||||
|
||||
// 准备确保分组 map 存在
|
||||
ensureGroup := func(name string) map[string]interface{} {
|
||||
v, ok := out[name]
|
||||
if !ok || v == nil {
|
||||
m := map[string]interface{}{}
|
||||
out[name] = m
|
||||
return m
|
||||
}
|
||||
if m, ok := v.(map[string]interface{}); ok {
|
||||
return m
|
||||
}
|
||||
// 若类型非 map,则覆盖为 map
|
||||
m := map[string]interface{}{}
|
||||
out[name] = m
|
||||
return m
|
||||
}
|
||||
|
||||
site := ensureGroup("site")
|
||||
login := ensureGroup("login")
|
||||
geo := ensureGroup("geo_ip")
|
||||
notif := ensureGroup("notification")
|
||||
record := ensureGroup("record")
|
||||
compact := ensureGroup("compact")
|
||||
nezha := func() map[string]interface{} {
|
||||
v, ok := compact["nezha"]
|
||||
if !ok || v == nil {
|
||||
m := map[string]interface{}{}
|
||||
compact["nezha"] = m
|
||||
return m
|
||||
}
|
||||
if m, ok := v.(map[string]interface{}); ok {
|
||||
return m
|
||||
}
|
||||
m := map[string]interface{}{}
|
||||
compact["nezha"] = m
|
||||
return m
|
||||
}()
|
||||
|
||||
// 扁平 -> 分组字段映射表
|
||||
move := func(flatKey string, group map[string]interface{}, groupKey string) {
|
||||
if v, ok := out[flatKey]; ok {
|
||||
group[groupKey] = v
|
||||
delete(out, flatKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Site
|
||||
move("sitename", site, "sitename")
|
||||
move("description", site, "description")
|
||||
move("allow_cors", site, "allow_cors")
|
||||
move("theme", site, "theme")
|
||||
move("private_site", site, "private_site")
|
||||
move("script_domain", site, "script_domain")
|
||||
move("send_ip_addr_to_guest", site, "send_ip_addr_to_guest")
|
||||
move("eula_accepted", site, "eula_accepted")
|
||||
move("custom_head", site, "custom_head")
|
||||
move("custom_body", site, "custom_body")
|
||||
|
||||
// Login
|
||||
move("api_key", login, "api_key")
|
||||
move("auto_discovery_key", login, "auto_discovery_key")
|
||||
move("o_auth_enabled", login, "o_auth_enabled")
|
||||
move("o_auth_provider", login, "o_auth_provider")
|
||||
move("disable_password_login", login, "disable_password_login")
|
||||
|
||||
// GeoIP
|
||||
move("geo_ip_enabled", geo, "geo_ip_enabled")
|
||||
move("geo_ip_provider", geo, "geo_ip_provider")
|
||||
|
||||
// Notification
|
||||
move("notification_enabled", notif, "notification_enabled")
|
||||
move("notification_method", notif, "notification_method")
|
||||
move("notification_template", notif, "notification_template")
|
||||
move("expire_notification_enabled", notif, "expire_notification_enabled")
|
||||
move("expire_notification_lead_days", notif, "expire_notification_lead_days")
|
||||
move("login_notification", notif, "login_notification")
|
||||
move("traffic_limit_percentage", notif, "traffic_limit_percentage")
|
||||
|
||||
// Record
|
||||
move("record_enabled", record, "record_enabled")
|
||||
move("record_preserve_time", record, "record_preserve_time")
|
||||
move("ping_record_preserve_time", record, "ping_record_preserve_time")
|
||||
|
||||
// Compact.Nezha
|
||||
move("nezha_compat_enabled", nezha, "nezha_compat_enabled")
|
||||
move("nezha_compat_listen", nezha, "nezha_compat_listen")
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// deepMerge 以 dst 为基础,将 src 合并覆盖到 dst。仅对 map[string]interface{} 递归。
|
||||
func deepMerge(dst, src map[string]interface{}) map[string]interface{} {
|
||||
if dst == nil {
|
||||
dst = map[string]interface{}{}
|
||||
}
|
||||
for k, v := range src {
|
||||
if v == nil {
|
||||
// 忽略空覆盖,避免误删值
|
||||
continue
|
||||
}
|
||||
if dv, ok := dst[k]; ok {
|
||||
dm, dIsMap := dv.(map[string]interface{})
|
||||
sm, sIsMap := v.(map[string]interface{})
|
||||
if dIsMap && sIsMap {
|
||||
dst[k] = deepMerge(dm, sm)
|
||||
continue
|
||||
}
|
||||
}
|
||||
dst[k] = v
|
||||
}
|
||||
return dst
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package conf
|
||||
|
||||
const (
|
||||
GeoIp_MMDB = "mmdb"
|
||||
GeoIp_IPAPI = "ip-api"
|
||||
GeoIp_GeoJS = "geojs"
|
||||
GeoIp_IPInfo = "ipinfo"
|
||||
GeoIp_Empty = "empty"
|
||||
Version_Development = "development"
|
||||
)
|
||||
@@ -0,0 +1,203 @@
|
||||
package conf
|
||||
|
||||
import "time"
|
||||
|
||||
var (
|
||||
Version = Version_Development
|
||||
CommitHash = "unknown"
|
||||
)
|
||||
|
||||
var (
|
||||
Conf *Config // 当直接修改时,请手动触发 eventType.ConfigUpdated 事件,或者使用 EditAndTrigger(func() { ... } 包裹
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Site Site `json:"site"`
|
||||
Login Login `json:"login"`
|
||||
GeoIp GeoIp `json:"geo_ip"`
|
||||
Notification Notification `json:"notification"`
|
||||
Record Record `json:"record"`
|
||||
Compact Compact `json:"compact"`
|
||||
}
|
||||
|
||||
type Site struct {
|
||||
Sitename string `json:"sitename"`
|
||||
Description string `json:"description"`
|
||||
AllowCors bool `json:"allow_cors"`
|
||||
PrivateSite bool `json:"private_site"` // 是否为私有站点,默认 false
|
||||
SendIpAddrToGuest bool `json:"send_ip_addr_to_guest"` // 是否向访客页面发送 IP 地址,默认 false
|
||||
ScriptDomain string `json:"script_domain"` // 自定义脚本域名
|
||||
EulaAccepted bool `json:"eula_accepted"`
|
||||
// 自定义美化
|
||||
CustomHead string `json:"custom_head"`
|
||||
CustomBody string `json:"custom_body"`
|
||||
Theme string `json:"theme"` // 主题名称,默认 'default'
|
||||
}
|
||||
|
||||
type Login struct {
|
||||
ApiKey string `json:"api_key"`
|
||||
AutoDiscoveryKey string `json:"auto_discovery_key"` // 自动发现密钥
|
||||
|
||||
// OAuth 配置
|
||||
OAuthEnabled bool `json:"o_auth_enabled"`
|
||||
OAuthProvider string `json:"o_auth_provider"`
|
||||
DisablePasswordLogin bool `json:"disable_password_login"`
|
||||
}
|
||||
|
||||
type GeoIp struct {
|
||||
GeoIpEnabled bool `json:"geo_ip_enabled"`
|
||||
GeoIpProvider string `json:"geo_ip_provider"` // empty, mmdb, ip-api, geojs
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
NotificationEnabled bool `json:"notification_enabled"` // 通知总开关
|
||||
NotificationMethod string `json:"notification_method"`
|
||||
NotificationTemplate string `json:"notification_template"`
|
||||
ExpireNotificationEnabled bool `json:"expire_notification_enabled"` // 是否启用过期通知
|
||||
ExpireNotificationLeadDays int `json:"expire_notification_lead_days"` // 过期前多少天通知,默认7天
|
||||
LoginNotification bool `json:"login_notification"` // 登录通知
|
||||
TrafficLimitPercentage float64 `json:"traffic_limit_percentage"` // 流量限制百分比,默认80.00%
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
RecordEnabled bool `json:"record_enabled"` // 是否启用记录功能
|
||||
RecordPreserveTime int `json:"record_preserve_time"` // 记录保留时间,单位小时,默认30天
|
||||
PingRecordPreserveTime int `json:"ping_record_preserve_time"` // Ping 记录保留时间,单位小时,默认1天
|
||||
}
|
||||
|
||||
type Compact struct {
|
||||
Nezha Nezha `json:"nezha"`
|
||||
}
|
||||
|
||||
type Nezha struct {
|
||||
// Nezha 兼容(Agent gRPC)
|
||||
NezhaCompatEnabled bool `json:"nezha_compat_enabled"`
|
||||
NezhaCompatListen string `json:"nezha_compat_listen"` // 例如 0.0.0.0:5555
|
||||
}
|
||||
|
||||
// [DEPRECATED] 旧的数据结构,将不再维护,请考虑使用 conf.Config 结构体
|
||||
type V1Struct struct {
|
||||
ID uint `json:"id,omitempty" gorm:"primaryKey;autoIncrement"` // 1
|
||||
Sitename string `json:"sitename" gorm:"type:varchar(100);not null"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
AllowCors bool `json:"allow_cors" gorm:"column:allow_cors;default:false"`
|
||||
Theme string `json:"theme" gorm:"type:varchar(100);default:'default'"` // 主题名称,默认 'default'
|
||||
PrivateSite bool `json:"private_site" gorm:"default:false"` // 是否为私有站点,默认 false
|
||||
ApiKey string `json:"api_key" gorm:"type:varchar(255);default:''"`
|
||||
AutoDiscoveryKey string `json:"auto_discovery_key" gorm:"type:varchar(255);default:''"` // 自动发现密钥
|
||||
ScriptDomain string `json:"script_domain" gorm:"type:varchar(255);default:''"` // 自定义脚本域名
|
||||
SendIpAddrToGuest bool `json:"send_ip_addr_to_guest" gorm:"default:false"` // 是否向访客页面发送 IP 地址,默认 false
|
||||
EulaAccepted bool `json:"eula_accepted" gorm:"default:false"`
|
||||
// GeoIP 配置
|
||||
GeoIpEnabled bool `json:"geo_ip_enabled" gorm:"default:true"`
|
||||
GeoIpProvider string `json:"geo_ip_provider" gorm:"type:varchar(20);default:'ip-api'"` // empty, mmdb, ip-api, geojs
|
||||
// Nezha 兼容(Agent gRPC)
|
||||
NezhaCompatEnabled bool `json:"nezha_compat_enabled" gorm:"default:false"`
|
||||
NezhaCompatListen string `json:"nezha_compat_listen" gorm:"type:varchar(100);default:''"` // 例如 0.0.0.0:5555
|
||||
// OAuth 配置
|
||||
OAuthEnabled bool `json:"o_auth_enabled" gorm:"default:false"`
|
||||
OAuthProvider string `json:"o_auth_provider" gorm:"type:varchar(50);default:'github'"`
|
||||
DisablePasswordLogin bool `json:"disable_password_login" gorm:"default:false"`
|
||||
// 自定义美化
|
||||
CustomHead string `json:"custom_head" gorm:"type:longtext"`
|
||||
CustomBody string `json:"custom_body" gorm:"type:longtext"`
|
||||
// 通知
|
||||
NotificationEnabled bool `json:"notification_enabled" gorm:"default:false"` // 通知总开关
|
||||
NotificationMethod string `json:"notification_method" gorm:"type:varchar(64);default:'none'"`
|
||||
NotificationTemplate string `json:"notification_template" gorm:"type:longtext;default:'{{emoji}}{{emoji}}{{emoji}}\nEvent: {{event}}\nClients: {{client}}\nMessage: {{message}}\nTime: {{time}}'"`
|
||||
ExpireNotificationEnabled bool `json:"expire_notification_enabled" gorm:"default:false"` // 是否启用过期通知
|
||||
ExpireNotificationLeadDays int `json:"expire_notification_lead_days" gorm:"default:7"` // 过期前多少天通知,默认7天
|
||||
LoginNotification bool `json:"login_notification" gorm:"default:false"` // 登录通知
|
||||
TrafficLimitPercentage float64 `json:"traffic_limit_percentage" gorm:"default:80.00"` // 流量限制百分比,默认80.00%
|
||||
// Record
|
||||
RecordEnabled bool `json:"record_enabled" gorm:"default:true"` // 是否启用记录功能
|
||||
RecordPreserveTime int `json:"record_preserve_time" gorm:"default:720"` // 记录保留时间,单位小时,默认30天
|
||||
PingRecordPreserveTime int `json:"ping_record_preserve_time" gorm:"default:24"` // Ping 记录保留时间,单位小时,默认1天
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (cst *V1Struct) ToConfig() Config {
|
||||
return Config{
|
||||
Site: Site{
|
||||
Sitename: cst.Sitename,
|
||||
Description: cst.Description,
|
||||
AllowCors: cst.AllowCors,
|
||||
PrivateSite: cst.PrivateSite,
|
||||
SendIpAddrToGuest: cst.SendIpAddrToGuest,
|
||||
ScriptDomain: cst.ScriptDomain,
|
||||
EulaAccepted: cst.EulaAccepted,
|
||||
CustomHead: cst.CustomHead,
|
||||
CustomBody: cst.CustomBody,
|
||||
Theme: cst.Theme,
|
||||
},
|
||||
Login: Login{
|
||||
ApiKey: cst.ApiKey,
|
||||
AutoDiscoveryKey: cst.AutoDiscoveryKey,
|
||||
OAuthEnabled: cst.OAuthEnabled,
|
||||
OAuthProvider: cst.OAuthProvider,
|
||||
DisablePasswordLogin: cst.DisablePasswordLogin,
|
||||
},
|
||||
GeoIp: GeoIp{
|
||||
GeoIpEnabled: cst.GeoIpEnabled,
|
||||
GeoIpProvider: cst.GeoIpProvider,
|
||||
},
|
||||
Notification: Notification{
|
||||
NotificationEnabled: cst.NotificationEnabled,
|
||||
NotificationMethod: cst.NotificationMethod,
|
||||
NotificationTemplate: cst.NotificationTemplate,
|
||||
ExpireNotificationEnabled: cst.ExpireNotificationEnabled,
|
||||
ExpireNotificationLeadDays: cst.ExpireNotificationLeadDays,
|
||||
LoginNotification: cst.LoginNotification,
|
||||
TrafficLimitPercentage: cst.TrafficLimitPercentage,
|
||||
},
|
||||
Record: Record{
|
||||
RecordEnabled: cst.RecordEnabled,
|
||||
RecordPreserveTime: cst.RecordPreserveTime,
|
||||
PingRecordPreserveTime: cst.PingRecordPreserveTime,
|
||||
},
|
||||
Compact: Compact{
|
||||
Nezha: Nezha{
|
||||
NezhaCompatEnabled: cst.NezhaCompatEnabled,
|
||||
NezhaCompatListen: cst.NezhaCompatListen,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Config) ToV1Format() V1Struct {
|
||||
return V1Struct{
|
||||
ID: 1,
|
||||
Sitename: cfg.Site.Sitename,
|
||||
Description: cfg.Site.Description,
|
||||
AllowCors: cfg.Site.AllowCors,
|
||||
Theme: cfg.Site.Theme,
|
||||
PrivateSite: cfg.Site.PrivateSite,
|
||||
ApiKey: cfg.Login.ApiKey,
|
||||
AutoDiscoveryKey: cfg.Login.AutoDiscoveryKey,
|
||||
ScriptDomain: cfg.Site.ScriptDomain,
|
||||
SendIpAddrToGuest: cfg.Site.SendIpAddrToGuest,
|
||||
EulaAccepted: cfg.Site.EulaAccepted,
|
||||
GeoIpEnabled: cfg.GeoIp.GeoIpEnabled,
|
||||
GeoIpProvider: cfg.GeoIp.GeoIpProvider,
|
||||
NezhaCompatEnabled: cfg.Compact.Nezha.NezhaCompatEnabled,
|
||||
NezhaCompatListen: cfg.Compact.Nezha.NezhaCompatListen,
|
||||
OAuthEnabled: cfg.Login.OAuthEnabled,
|
||||
OAuthProvider: cfg.Login.OAuthProvider,
|
||||
DisablePasswordLogin: cfg.Login.DisablePasswordLogin,
|
||||
CustomHead: cfg.Site.CustomHead,
|
||||
CustomBody: cfg.Site.CustomBody,
|
||||
NotificationEnabled: cfg.Notification.NotificationEnabled,
|
||||
NotificationMethod: cfg.Notification.NotificationMethod,
|
||||
NotificationTemplate: cfg.Notification.NotificationTemplate,
|
||||
ExpireNotificationEnabled: cfg.Notification.ExpireNotificationEnabled,
|
||||
ExpireNotificationLeadDays: cfg.Notification.ExpireNotificationLeadDays,
|
||||
LoginNotification: cfg.Notification.LoginNotification,
|
||||
TrafficLimitPercentage: cfg.Notification.TrafficLimitPercentage,
|
||||
RecordEnabled: cfg.Record.RecordEnabled,
|
||||
RecordPreserveTime: cfg.Record.RecordPreserveTime,
|
||||
PingRecordPreserveTime: cfg.Record.PingRecordPreserveTime,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/dbcore"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
messageevent "github.com/komari-monitor/komari/internal/database/models/messageEvent"
|
||||
@@ -39,7 +39,7 @@ func CreateSession(uuid string, expires int, userAgent, ip, login_method string)
|
||||
LoginMethod: login_method,
|
||||
LatestOnline: models.FromTime(time.Now()),
|
||||
}
|
||||
cfg, _ := config.Get()
|
||||
cfg, _ := conf.GetWithV1Format()
|
||||
if cfg.LoginNotification {
|
||||
ipAddr := net.ParseIP(ip)
|
||||
ipinfo, _ := geoip.GetGeoInfo(ipAddr)
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gookit/event"
|
||||
"github.com/komari-monitor/komari/internal/database/dbcore"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"github.com/komari-monitor/komari/internal/eventType"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var mu sync.Mutex
|
||||
|
||||
func init() {
|
||||
mu = sync.Mutex{}
|
||||
}
|
||||
|
||||
func Get() (models.Config, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
db := dbcore.GetDBInstance()
|
||||
var config models.Config
|
||||
if err := db.First(&config).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
config = models.Config{
|
||||
ID: 1,
|
||||
Sitename: "Komari",
|
||||
Description: "Komari Monitor, a simple server monitoring tool.",
|
||||
AllowCors: false,
|
||||
OAuthEnabled: false,
|
||||
GeoIpEnabled: true,
|
||||
GeoIpProvider: "ipinfo",
|
||||
NezhaCompatEnabled: false,
|
||||
NezhaCompatListen: "",
|
||||
NotificationTemplate: "{{emoji}}{{emoji}}{{emoji}}\nEvent: {{event}}\nClients: {{client}}\nMessage: {{message}}\nTime: {{time}}",
|
||||
UpdatedAt: models.FromTime(time.Now()),
|
||||
CreatedAt: models.FromTime(time.Now()),
|
||||
}
|
||||
if err := db.Create(&config).Error; err != nil {
|
||||
log.Fatal("Failed to create default config:", err)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
return config, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func Save(cst models.Config) error {
|
||||
db := dbcore.GetDBInstance()
|
||||
oldConfig, _ := Get()
|
||||
// Only one records
|
||||
cst.ID = 1
|
||||
cst.UpdatedAt = models.FromTime(time.Now())
|
||||
if err := db.Model(&models.Config{}).Where("id = ?", cst.ID).
|
||||
Select("*").
|
||||
Updates(cst).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
newConfig, _ := Get()
|
||||
event.Trigger(eventType.ConfigUpdated, event.M{"old": oldConfig, "new": newConfig})
|
||||
return nil
|
||||
}
|
||||
|
||||
func Update(cst map[string]interface{}) error {
|
||||
db := dbcore.GetDBInstance()
|
||||
oldConfig, _ := Get()
|
||||
// Proceed with update
|
||||
cst["id"] = 1
|
||||
cst["updated_at"] = time.Now()
|
||||
delete(cst, "created_at")
|
||||
delete(cst, "CreatedAt")
|
||||
|
||||
// 至少有一种登录方式启用
|
||||
newDisablePasswordLogin := oldConfig.DisablePasswordLogin
|
||||
newOAuthEnabled := oldConfig.OAuthEnabled
|
||||
if val, exists := cst["disable_password_login"]; exists {
|
||||
newDisablePasswordLogin = val.(bool)
|
||||
}
|
||||
if val, exists := cst["o_auth_enabled"]; exists {
|
||||
newOAuthEnabled = val.(bool)
|
||||
}
|
||||
if newDisablePasswordLogin && !newOAuthEnabled {
|
||||
return errors.New("at least one login method must be enabled (password/oauth)")
|
||||
}
|
||||
// 没绑定账号也不能禁用
|
||||
if newDisablePasswordLogin {
|
||||
usr := &models.User{}
|
||||
if err := db.Model(&models.User{}).First(usr).Error; err != nil {
|
||||
return errors.Join(err, errors.New("failed to retrieve user"))
|
||||
}
|
||||
if usr.SSOID == "" {
|
||||
return errors.New("cannot disable password login when no SSO-bound account exists")
|
||||
}
|
||||
}
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&models.Config{}).Where("id = ?", oldConfig.ID).Updates(cst).Error; err != nil {
|
||||
return errors.Join(err, errors.New("failed to update configuration"))
|
||||
}
|
||||
newConfig := &models.Config{}
|
||||
if err := tx.Where("id = ?", oldConfig.ID).First(newConfig).Error; err != nil {
|
||||
return errors.Join(err, errors.New("failed to retrieve updated configuration"))
|
||||
}
|
||||
event.Trigger(eventType.ConfigUpdated, event.M{"old": oldConfig, "new": *newConfig})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,19 +1,11 @@
|
||||
package dbcore
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/komari-monitor/komari/cmd/flags"
|
||||
"github.com/komari-monitor/komari/internal/common"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
logutil "github.com/komari-monitor/komari/internal/log"
|
||||
"gorm.io/driver/mysql"
|
||||
@@ -21,338 +13,6 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// zipDirectoryExcluding 将 srcDir 打包为 dstZip,exclude 是绝对路径集合需要排除
|
||||
func zipDirectoryExcluding(srcDir, dstZip string, exclude map[string]struct{}) error {
|
||||
// 规范化排除路径为绝对路径
|
||||
normExclude := make(map[string]struct{}, len(exclude))
|
||||
for p := range exclude {
|
||||
abs, _ := filepath.Abs(p)
|
||||
normExclude[abs] = struct{}{}
|
||||
}
|
||||
|
||||
out, err := os.Create(dstZip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
zw := zip.NewWriter(out)
|
||||
defer zw.Close()
|
||||
|
||||
absSrc, _ := filepath.Abs(srcDir)
|
||||
walkErr := filepath.Walk(absSrc, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 排除 backup.zip 本身
|
||||
if _, ok := normExclude[path]; ok {
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// 计算 zip 内相对路径
|
||||
rel, err := filepath.Rel(absSrc, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 根目录跳过
|
||||
if rel == "." {
|
||||
return nil
|
||||
}
|
||||
// 替换为正斜杠
|
||||
zipName := filepath.ToSlash(rel)
|
||||
|
||||
if info.IsDir() {
|
||||
_, err := zw.Create(zipName + "/")
|
||||
return err
|
||||
}
|
||||
// 普通文件
|
||||
fh, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w, err := zw.Create(zipName)
|
||||
if err != nil {
|
||||
fh.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(w, fh); err != nil {
|
||||
fh.Close()
|
||||
return err
|
||||
}
|
||||
fh.Close()
|
||||
return nil
|
||||
})
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
return zw.Close()
|
||||
}
|
||||
|
||||
// removeAllInDirExcept 删除 dir 下除 exclude 指定绝对路径外的所有文件和文件夹
|
||||
func removeAllInDirExcept(dir string, exclude map[string]struct{}) error {
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
normExclude := make(map[string]struct{}, len(exclude))
|
||||
for p := range exclude {
|
||||
abs, _ := filepath.Abs(p)
|
||||
normExclude[abs] = struct{}{}
|
||||
}
|
||||
entries, err := os.ReadDir(absDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, e := range entries {
|
||||
full := filepath.Join(absDir, e.Name())
|
||||
if _, ok := normExclude[full]; ok {
|
||||
continue
|
||||
}
|
||||
if err := os.RemoveAll(full); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// unzipToDir 将 zipPath 解压到 dstDir,包含路径遍历保护
|
||||
func unzipToDir(zipPath, dstDir string) error {
|
||||
zr, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer zr.Close()
|
||||
|
||||
if err := os.MkdirAll(dstDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
absDst, _ := filepath.Abs(dstDir)
|
||||
|
||||
for _, f := range zr.File {
|
||||
// 构造目标路径并做路径遍历保护
|
||||
cleanName := filepath.Clean(f.Name)
|
||||
targetPath := filepath.Join(absDst, cleanName)
|
||||
if !strings.HasPrefix(targetPath, absDst+string(os.PathSeparator)) && targetPath != absDst {
|
||||
return fmt.Errorf("illegal file path in zip: %s", f.Name)
|
||||
}
|
||||
if f.FileInfo().IsDir() {
|
||||
if err := os.MkdirAll(targetPath, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
rc.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(out, rc); err != nil {
|
||||
out.Close()
|
||||
rc.Close()
|
||||
return err
|
||||
}
|
||||
out.Close()
|
||||
rc.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeClientInfo 将旧版ClientInfo数据迁移到新版Client表
|
||||
func mergeClientInfo(db *gorm.DB) {
|
||||
var clientInfos []common.ClientInfo
|
||||
if err := db.Find(&clientInfos).Error; err != nil {
|
||||
log.Printf("Failed to read ClientInfo table: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, info := range clientInfos {
|
||||
var client models.Client
|
||||
if err := db.Where("uuid = ?", info.UUID).First(&client).Error; err != nil {
|
||||
log.Printf("Could not find Client record with UUID %s: %v", info.UUID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 更新Client记录
|
||||
client.Name = info.Name
|
||||
client.CpuName = info.CpuName
|
||||
client.Virtualization = info.Virtualization
|
||||
client.Arch = info.Arch
|
||||
client.CpuCores = info.CpuCores
|
||||
client.OS = info.OS
|
||||
client.GpuName = info.GpuName
|
||||
client.IPv4 = info.IPv4
|
||||
client.IPv6 = info.IPv6
|
||||
client.Region = info.Region
|
||||
client.Remark = info.Remark
|
||||
client.PublicRemark = info.PublicRemark
|
||||
client.MemTotal = info.MemTotal
|
||||
client.SwapTotal = info.SwapTotal
|
||||
client.DiskTotal = info.DiskTotal
|
||||
client.Version = info.Version
|
||||
client.Weight = info.Weight
|
||||
client.Price = info.Price
|
||||
client.BillingCycle = info.BillingCycle
|
||||
client.ExpiredAt = models.FromTime(info.ExpiredAt)
|
||||
// Save updated Client record
|
||||
if err := db.Save(&client).Error; err != nil {
|
||||
log.Printf("Failed to update Client record: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Backup and rename old table after migration
|
||||
if err := db.Migrator().RenameTable("client_infos", "client_infos_backup"); err != nil {
|
||||
log.Printf("Failed to backup ClientInfo table: %v", err)
|
||||
return
|
||||
}
|
||||
log.Println("Data migration completed, old table has been backed up as client_infos_backup")
|
||||
}
|
||||
|
||||
func MergeDatabase(db *gorm.DB) {
|
||||
if db.Migrator().HasTable("client_infos") {
|
||||
log.Println("[>0.0.5] Legacy ClientInfo table detected, starting data migration...")
|
||||
mergeClientInfo(db)
|
||||
}
|
||||
if db.Migrator().HasColumn(&models.Config{}, "allow_cros") {
|
||||
log.Println("[>0.0.5a] Renaming column 'allow_cros' to 'allow_cors' in config table...")
|
||||
db.Migrator().RenameColumn(&models.Config{}, "allow_cros", "allow_cors")
|
||||
}
|
||||
if db.Migrator().HasColumn(&models.LoadNotification{}, "client") {
|
||||
log.Println("[>0.1.4] Rebuilding LoadNotification table....")
|
||||
db.Migrator().DropTable(&models.LoadNotification{})
|
||||
}
|
||||
if !db.Migrator().HasTable(&models.OidcProvider{}) && db.Migrator().HasTable(&models.Config{}) {
|
||||
log.Println("[>1.0.2] Merge OidcProvider table....")
|
||||
var config struct {
|
||||
OAuthClientID string `json:"o_auth_client_id" gorm:"type:varchar(255)"`
|
||||
OAuthClientSecret string `json:"o_auth_client_secret" gorm:"type:varchar(255)"`
|
||||
}
|
||||
if err := db.Raw("SELECT * FROM configs LIMIT 1").Scan(&config).Error; err != nil {
|
||||
log.Println("Failed to get config for OIDC provider migration:", err)
|
||||
}
|
||||
db.AutoMigrate(&models.OidcProvider{})
|
||||
j, err := json.Marshal(&map[string]string{
|
||||
"client_id": config.OAuthClientID,
|
||||
"client_secret": config.OAuthClientSecret,
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("Failed to marshal OIDC provider config:", err)
|
||||
return
|
||||
}
|
||||
db.Save(&models.OidcProvider{
|
||||
Name: "github",
|
||||
Addition: string(j),
|
||||
})
|
||||
db.AutoMigrate(&models.Config{})
|
||||
db.Model(&models.Config{}).Where("id = 1").Update("o_auth_provider", "github")
|
||||
}
|
||||
if !db.Migrator().HasTable(&models.MessageSenderProvider{}) && db.Migrator().HasTable(&models.Config{}) {
|
||||
log.Println("[>1.0.2] Migrate MessageSender configuration....")
|
||||
var config struct {
|
||||
TelegramBotToken string `json:"telegram_bot_token" gorm:"type:varchar(255)"`
|
||||
TelegramChatID string `json:"telegram_chat_id" gorm:"type:varchar(255)"`
|
||||
TelegramEndpoint string `json:"telegram_endpoint" gorm:"type:varchar(255)"`
|
||||
EmailHost string `json:"email_host" gorm:"type:varchar(255)"`
|
||||
EmailPort int `json:"email_port" gorm:"type:int"`
|
||||
EmailUsername string `json:"email_username" gorm:"type:varchar(255)"`
|
||||
EmailPassword string `json:"email_password" gorm:"type:varchar(255)"`
|
||||
EmailSender string `json:"email_sender" gorm:"type:varchar(255)"`
|
||||
EmailReceiver string `json:"email_receiver" gorm:"type:varchar(255)"`
|
||||
EmailUseSSL bool `json:"email_use_ssl" gorm:"type:boolean"`
|
||||
NotificationMethod string `json:"notification_method" gorm:"type:varchar(50)"`
|
||||
}
|
||||
if err := db.Raw("SELECT * FROM configs LIMIT 1").Scan(&config).Error; err != nil {
|
||||
log.Println("Failed to get config for MessageSender migration:", err)
|
||||
}
|
||||
|
||||
db.AutoMigrate(&models.MessageSenderProvider{})
|
||||
|
||||
// 迁移Telegram配置
|
||||
if config.NotificationMethod == "telegram" && config.TelegramBotToken != "" {
|
||||
telegramConfig := map[string]interface{}{
|
||||
"bot_token": config.TelegramBotToken,
|
||||
"chat_id": config.TelegramChatID,
|
||||
"endpoint": config.TelegramEndpoint,
|
||||
}
|
||||
if telegramConfig["endpoint"] == "" {
|
||||
telegramConfig["endpoint"] = "https://api.telegram.org/bot"
|
||||
}
|
||||
telegramConfigJSON, err := json.Marshal(telegramConfig)
|
||||
if err != nil {
|
||||
log.Println("Failed to marshal Telegram config:", err)
|
||||
} else {
|
||||
db.Save(&models.MessageSenderProvider{
|
||||
Name: "telegram",
|
||||
Addition: string(telegramConfigJSON),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 迁移Email配置
|
||||
if config.NotificationMethod == "email" && config.EmailHost != "" {
|
||||
emailConfig := map[string]interface{}{
|
||||
"host": config.EmailHost,
|
||||
"port": config.EmailPort,
|
||||
"username": config.EmailUsername,
|
||||
"password": config.EmailPassword,
|
||||
"sender": config.EmailSender,
|
||||
"receiver": config.EmailReceiver,
|
||||
"use_ssl": config.EmailUseSSL,
|
||||
}
|
||||
emailConfigJSON, err := json.Marshal(emailConfig)
|
||||
if err != nil {
|
||||
log.Println("Failed to marshal Email config:", err)
|
||||
} else {
|
||||
db.Save(&models.MessageSenderProvider{
|
||||
Name: "email",
|
||||
Addition: string(emailConfigJSON),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 删除旧的配置字段
|
||||
if db.Migrator().HasColumn(&models.Config{}, "telegram_bot_token") {
|
||||
db.Migrator().DropColumn(&models.Config{}, "telegram_bot_token")
|
||||
}
|
||||
if db.Migrator().HasColumn(&models.Config{}, "telegram_chat_id") {
|
||||
db.Migrator().DropColumn(&models.Config{}, "telegram_chat_id")
|
||||
}
|
||||
if db.Migrator().HasColumn(&models.Config{}, "telegram_endpoint") {
|
||||
db.Migrator().DropColumn(&models.Config{}, "telegram_endpoint")
|
||||
}
|
||||
if db.Migrator().HasColumn(&models.Config{}, "email_host") {
|
||||
db.Migrator().DropColumn(&models.Config{}, "email_host")
|
||||
}
|
||||
if db.Migrator().HasColumn(&models.Config{}, "email_port") {
|
||||
db.Migrator().DropColumn(&models.Config{}, "email_port")
|
||||
}
|
||||
if db.Migrator().HasColumn(&models.Config{}, "email_username") {
|
||||
db.Migrator().DropColumn(&models.Config{}, "email_username")
|
||||
}
|
||||
if db.Migrator().HasColumn(&models.Config{}, "email_password") {
|
||||
db.Migrator().DropColumn(&models.Config{}, "email_password")
|
||||
}
|
||||
if db.Migrator().HasColumn(&models.Config{}, "email_sender") {
|
||||
db.Migrator().DropColumn(&models.Config{}, "email_sender")
|
||||
}
|
||||
if db.Migrator().HasColumn(&models.Config{}, "email_receiver") {
|
||||
db.Migrator().DropColumn(&models.Config{}, "email_receiver")
|
||||
}
|
||||
if db.Migrator().HasColumn(&models.Config{}, "email_use_ssl") {
|
||||
db.Migrator().DropColumn(&models.Config{}, "email_use_ssl")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
instance *gorm.DB
|
||||
once sync.Once
|
||||
@@ -362,50 +22,6 @@ func GetDBInstance() *gorm.DB {
|
||||
once.Do(func() {
|
||||
var err error
|
||||
|
||||
// 在数据库初始化前执行:如果存在 ./data/backup.zip,则进行恢复逻辑
|
||||
func() {
|
||||
backupZipPath := filepath.Join(".", "data", "backup.zip")
|
||||
if _, statErr := os.Stat(backupZipPath); statErr == nil {
|
||||
// 4. 把除了 ./data/backup.zip 之外的所有文件压缩到 ./backup/{time}.zip
|
||||
if err := os.MkdirAll("./backup", 0755); err != nil {
|
||||
log.Printf("[restore] failed to create backup dir: %v", err)
|
||||
} else {
|
||||
tsName := time.Now().Format("20060102-150405")
|
||||
bakPath := filepath.Join("./backup", fmt.Sprintf("%s.zip", tsName))
|
||||
if zipErr := zipDirectoryExcluding("./data", bakPath, map[string]struct{}{backupZipPath: {}}); zipErr != nil {
|
||||
log.Printf("[restore] failed to zip current data: %v", zipErr)
|
||||
} else {
|
||||
log.Printf("[restore] current data zipped to %s", bakPath)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 删除除了 ./data/backup.zip 之外的所有文件
|
||||
if delErr := removeAllInDirExcept("./data", map[string]struct{}{backupZipPath: {}}); delErr != nil {
|
||||
log.Printf("[restore] failed to cleanup data dir: %v", delErr)
|
||||
}
|
||||
|
||||
// 6. 解压 ./data/backup.zip 到 ./data
|
||||
if unzipErr := unzipToDir(backupZipPath, "./data"); unzipErr != nil {
|
||||
log.Printf("[restore] failed to unzip backup into data: %v", unzipErr)
|
||||
} else {
|
||||
log.Printf("[restore] backup.zip extracted to ./data")
|
||||
}
|
||||
|
||||
// 7. 删除 ./data/backup.zip
|
||||
if rmErr := os.Remove(backupZipPath); rmErr != nil {
|
||||
log.Printf("[restore] failed to remove backup.zip: %v", rmErr)
|
||||
} else {
|
||||
log.Printf("[restore] backup.zip removed")
|
||||
}
|
||||
// 8. 删除标记
|
||||
if rmErr := os.Remove("./data/komari-backup-markup"); rmErr != nil {
|
||||
log.Printf("[restore] failed to remove komari-backup-markup: %v", rmErr)
|
||||
} else {
|
||||
log.Printf("[restore] komari-backup-markup removed")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
logConfig := &gorm.Config{
|
||||
Logger: logutil.NewGormLogger(),
|
||||
}
|
||||
@@ -441,14 +57,13 @@ func GetDBInstance() *gorm.DB {
|
||||
default:
|
||||
log.Fatalf("Unsupported database type: %s", flags.DatabaseType)
|
||||
}
|
||||
MergeDatabase(instance)
|
||||
// 自动迁移模型
|
||||
err = instance.AutoMigrate(
|
||||
&models.User{},
|
||||
&models.Client{},
|
||||
&models.Record{},
|
||||
&models.GPURecord{},
|
||||
&models.Config{},
|
||||
//&conf.V1Struct{},
|
||||
&models.Log{},
|
||||
&models.Clipboard{},
|
||||
&models.LoadNotification{},
|
||||
|
||||
@@ -1,42 +1,4 @@
|
||||
package models
|
||||
|
||||
type Config struct {
|
||||
ID uint `json:"id,omitempty" gorm:"primaryKey;autoIncrement"` // 1
|
||||
Sitename string `json:"sitename" gorm:"type:varchar(100);not null"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
AllowCors bool `json:"allow_cors" gorm:"column:allow_cors;default:false"`
|
||||
Theme string `json:"theme" gorm:"type:varchar(100);default:'default'"` // 主题名称,默认 'default'
|
||||
PrivateSite bool `json:"private_site" gorm:"default:false"` // 是否为私有站点,默认 false
|
||||
ApiKey string `json:"api_key" gorm:"type:varchar(255);default:''"`
|
||||
AutoDiscoveryKey string `json:"auto_discovery_key" gorm:"type:varchar(255);default:''"` // 自动发现密钥
|
||||
ScriptDomain string `json:"script_domain" gorm:"type:varchar(255);default:''"` // 自定义脚本域名
|
||||
SendIpAddrToGuest bool `json:"send_ip_addr_to_guest" gorm:"default:false"` // 是否向访客页面发送 IP 地址,默认 false
|
||||
EulaAccepted bool `json:"eula_accepted" gorm:"default:false"`
|
||||
// GeoIP 配置
|
||||
GeoIpEnabled bool `json:"geo_ip_enabled" gorm:"default:true"`
|
||||
GeoIpProvider string `json:"geo_ip_provider" gorm:"type:varchar(20);default:'ip-api'"` // empty, mmdb, ip-api, geojs
|
||||
// Nezha 兼容(Agent gRPC)
|
||||
NezhaCompatEnabled bool `json:"nezha_compat_enabled" gorm:"default:false"`
|
||||
NezhaCompatListen string `json:"nezha_compat_listen" gorm:"type:varchar(100);default:''"` // 例如 0.0.0.0:5555
|
||||
// OAuth 配置
|
||||
OAuthEnabled bool `json:"o_auth_enabled" gorm:"default:false"`
|
||||
OAuthProvider string `json:"o_auth_provider" gorm:"type:varchar(50);default:'github'"`
|
||||
DisablePasswordLogin bool `json:"disable_password_login" gorm:"default:false"`
|
||||
// 自定义美化
|
||||
CustomHead string `json:"custom_head" gorm:"type:longtext"`
|
||||
CustomBody string `json:"custom_body" gorm:"type:longtext"`
|
||||
// 通知
|
||||
NotificationEnabled bool `json:"notification_enabled" gorm:"default:false"` // 通知总开关
|
||||
NotificationMethod string `json:"notification_method" gorm:"type:varchar(64);default:'none'"`
|
||||
NotificationTemplate string `json:"notification_template" gorm:"type:longtext;default:'{{emoji}}{{emoji}}{{emoji}}\nEvent: {{event}}\nClients: {{client}}\nMessage: {{message}}\nTime: {{time}}'"`
|
||||
ExpireNotificationEnabled bool `json:"expire_notification_enabled" gorm:"default:false"` // 是否启用过期通知
|
||||
ExpireNotificationLeadDays int `json:"expire_notification_lead_days" gorm:"default:7"` // 过期前多少天通知,默认7天
|
||||
LoginNotification bool `json:"login_notification" gorm:"default:false"` // 登录通知
|
||||
TrafficLimitPercentage float64 `json:"traffic_limit_percentage" gorm:"default:80.00"` // 流量限制百分比,默认80.00%
|
||||
// Record
|
||||
RecordEnabled bool `json:"record_enabled" gorm:"default:true"` // 是否启用记录功能
|
||||
RecordPreserveTime int `json:"record_preserve_time" gorm:"default:720"` // 记录保留时间,单位小时,默认30天
|
||||
PingRecordPreserveTime int `json:"ping_record_preserve_time" gorm:"default:24"` // Ping 记录保留时间,单位小时,默认1天
|
||||
CreatedAt LocalTime
|
||||
UpdatedAt LocalTime
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/komari-monitor/komari/cmd/flags"
|
||||
"github.com/komari-monitor/komari/internal/database/dbcore"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
)
|
||||
@@ -171,14 +170,6 @@ func CompactRecord() error {
|
||||
log.Printf("Error migrating GPU records: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if flags.DatabaseType == "sqlite" {
|
||||
if err := db.Exec("VACUUM").Error; err != nil {
|
||||
log.Printf("Error vacuuming database: %v", err)
|
||||
}
|
||||
db.Exec("PRAGMA wal_checkpoint(TRUNCATE);")
|
||||
}
|
||||
//log.Printf("Record compaction completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/dbcore"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
)
|
||||
|
||||
func GetPublicInfo() (any, error) {
|
||||
cst, err := config.Get()
|
||||
cst, err := conf.GetWithV1Format()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
@@ -52,14 +52,14 @@ func GetRegionUnicodeEmoji(isoCode string) string {
|
||||
}
|
||||
|
||||
func InitGeoIp() {
|
||||
conf, err := config.Get()
|
||||
config, err := conf.GetWithV1Format()
|
||||
if err != nil {
|
||||
panic("Failed to get configuration for GeoIP: " + err.Error())
|
||||
}
|
||||
if !conf.GeoIpEnabled {
|
||||
if !config.GeoIpEnabled {
|
||||
return
|
||||
}
|
||||
switch conf.GeoIpProvider {
|
||||
switch config.GeoIpProvider {
|
||||
case "mmdb":
|
||||
NewCurrentProvider, err := NewMaxMindGeoIPService()
|
||||
if err != nil {
|
||||
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
"github.com/komari-monitor/komari/internal/api_v1/client"
|
||||
"github.com/komari-monitor/komari/internal/api_v1/record"
|
||||
"github.com/komari-monitor/komari/internal/api_v1/task"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
)
|
||||
|
||||
func LoadApiV1Routes(r *gin.Engine, conf models.Config) {
|
||||
func LoadApiV1Routes(r *gin.Engine, conf conf.V1Struct) {
|
||||
r.Use(func(c *gin.Context) {
|
||||
if len(c.Request.URL.Path) >= 4 && c.Request.URL.Path[:4] == "/api" {
|
||||
c.Header("Cache-Control", "no-store")
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/komari-monitor/komari/internal/version"
|
||||
"gorm.io/gorm"
|
||||
gormlogger "gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/utils"
|
||||
@@ -25,12 +24,7 @@ func NewGormLogger() *GormLogger {
|
||||
return &GormLogger{
|
||||
SlowThreshold: 200 * time.Millisecond,
|
||||
IgnoreRecordNotFoundError: true,
|
||||
LogLevel: func(hash string) gormlogger.LogLevel {
|
||||
if hash == "unknown" {
|
||||
return gormlogger.Info
|
||||
}
|
||||
return gormlogger.Silent
|
||||
}(version.VersionHash),
|
||||
LogLevel: gormlogger.Warn,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -152,7 +152,7 @@ func SetupGlobalLogger(level slog.Level) {
|
||||
stdlog.SetPrefix("")
|
||||
|
||||
// 替换标准库 log 的输出为 slog handler
|
||||
stdlog.SetOutput(&writerAdapter{handler: handler, level: level})
|
||||
stdlog.SetOutput(&writerAdapter{handler: handler, level: slog.LevelInfo})
|
||||
}
|
||||
|
||||
// writerAdapter 将标准库 log 的输出适配到 slog
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database"
|
||||
"github.com/komari-monitor/komari/internal/database/auditlog"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"github.com/komari-monitor/komari/internal/messageSender/factory"
|
||||
)
|
||||
@@ -52,7 +52,7 @@ func Initialize() {
|
||||
}
|
||||
})
|
||||
}()
|
||||
cfg, _ := config.Get()
|
||||
cfg, _ := conf.GetWithV1Format()
|
||||
|
||||
if cfg.NotificationMethod == "" || cfg.NotificationMethod == "none" {
|
||||
LoadProvider("empty", "{}")
|
||||
@@ -74,7 +74,7 @@ func SendTextMessage(message string, title string) error {
|
||||
return fmt.Errorf("message sender provider is not initialized")
|
||||
}
|
||||
var err error
|
||||
cfg, err := config.Get()
|
||||
cfg, err := conf.GetWithV1Format()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -96,7 +96,7 @@ func SendEvent(event models.EventMessage) error {
|
||||
return fmt.Errorf("message sender provider is not initialized")
|
||||
}
|
||||
var err error
|
||||
cfg, err := config.Get()
|
||||
cfg, err := conf.GetWithV1Format()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/clients"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
messageevent "github.com/komari-monitor/komari/internal/database/models/messageEvent"
|
||||
"github.com/komari-monitor/komari/internal/messageSender"
|
||||
@@ -23,7 +23,7 @@ func CheckExpireScheduledWork() {
|
||||
duration := next.Sub(now)
|
||||
time.Sleep(duration)
|
||||
|
||||
cfg, err := config.Get()
|
||||
cfg, err := conf.GetWithV1Format()
|
||||
if err != nil {
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/clients"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/database/dbcore"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
messageevent "github.com/komari-monitor/komari/internal/database/models/messageEvent"
|
||||
@@ -32,7 +32,7 @@ var clientStates sync.Map
|
||||
// getNotificationConfig 获取指定客户端的通知配置。
|
||||
// 返回配置对象和一个布尔值,指示全局和该客户端是否启用通知。
|
||||
func getNotificationConfig(clientID string) (*models.OfflineNotification, bool) {
|
||||
conf, err := config.Get()
|
||||
conf, err := conf.GetWithV1Format()
|
||||
if err != nil || !conf.NotificationEnabled {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/clients"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"github.com/komari-monitor/komari/internal/messageSender"
|
||||
"github.com/komari-monitor/komari/internal/ws"
|
||||
@@ -27,7 +27,7 @@ func CheckTraffic() {
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := config.Get()
|
||||
cfg, err := conf.GetWithV1Format()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"github.com/komari-monitor/komari/internal/oauth/factory"
|
||||
)
|
||||
@@ -70,7 +70,7 @@ func Initialize() error {
|
||||
}
|
||||
}
|
||||
})
|
||||
cfg, _ := config.Get()
|
||||
cfg, _ := conf.GetWithV1Format()
|
||||
if cfg.OAuthProvider == "" || cfg.OAuthProvider == "none" {
|
||||
LoadProvider("empty", "{}")
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/dbcore"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
)
|
||||
|
||||
func ApplyPatch() {
|
||||
db := dbcore.GetDBInstance()
|
||||
// 0.0.5 迁移ClientInfo
|
||||
if db.Migrator().HasTable("client_infos") {
|
||||
v0_0_5(db)
|
||||
}
|
||||
// 0.0.5a 修正cors拼写错误
|
||||
if db.Migrator().HasColumn(&conf.V1Struct{}, "allow_cros") {
|
||||
v0_0_5a(db)
|
||||
}
|
||||
// 0.1.4 重建LoadNotification表
|
||||
if db.Migrator().HasColumn(&models.LoadNotification{}, "client") {
|
||||
log.Println("[>0.1.4] Rebuilding LoadNotification table....")
|
||||
db.Migrator().DropTable(&models.LoadNotification{})
|
||||
}
|
||||
// 1.0.2 合并OIDC提供商表
|
||||
if !db.Migrator().HasTable(&models.OidcProvider{}) && db.Migrator().HasTable(&conf.V1Struct{}) {
|
||||
v1_0_2_Oidc(db)
|
||||
}
|
||||
// 1.0.2 迁移消息发送配置到单独的表
|
||||
if !db.Migrator().HasTable(&models.MessageSenderProvider{}) && db.Migrator().HasTable(&conf.V1Struct{}) {
|
||||
v1_0_2_MessageSender(db)
|
||||
}
|
||||
// 1.1.4 迁移配置表
|
||||
if db.Migrator().HasColumn(&conf.V1Struct{}, "id") {
|
||||
v1_1_4(db)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/komari-monitor/komari/internal/common"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func v0_0_5(db *gorm.DB) {
|
||||
log.Println("[>0.0.5] Legacy ClientInfo table detected, starting data migration...")
|
||||
var clientInfos []common.ClientInfo
|
||||
if err := db.Find(&clientInfos).Error; err != nil {
|
||||
log.Printf("Failed to read ClientInfo table: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, info := range clientInfos {
|
||||
var client models.Client
|
||||
if err := db.Where("uuid = ?", info.UUID).First(&client).Error; err != nil {
|
||||
log.Printf("Could not find Client record with UUID %s: %v", info.UUID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 更新Client记录
|
||||
client.Name = info.Name
|
||||
client.CpuName = info.CpuName
|
||||
client.Virtualization = info.Virtualization
|
||||
client.Arch = info.Arch
|
||||
client.CpuCores = info.CpuCores
|
||||
client.OS = info.OS
|
||||
client.GpuName = info.GpuName
|
||||
client.IPv4 = info.IPv4
|
||||
client.IPv6 = info.IPv6
|
||||
client.Region = info.Region
|
||||
client.Remark = info.Remark
|
||||
client.PublicRemark = info.PublicRemark
|
||||
client.MemTotal = info.MemTotal
|
||||
client.SwapTotal = info.SwapTotal
|
||||
client.DiskTotal = info.DiskTotal
|
||||
client.Version = info.Version
|
||||
client.Weight = info.Weight
|
||||
client.Price = info.Price
|
||||
client.BillingCycle = info.BillingCycle
|
||||
client.ExpiredAt = models.FromTime(info.ExpiredAt)
|
||||
// Save updated Client record
|
||||
if err := db.Save(&client).Error; err != nil {
|
||||
log.Printf("Failed to update Client record: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Backup and rename old table after migration
|
||||
if err := db.Migrator().RenameTable("client_infos", "client_infos_backup"); err != nil {
|
||||
log.Printf("Failed to backup ClientInfo table: %v", err)
|
||||
return
|
||||
}
|
||||
log.Println("Data migration completed, old table has been backed up as client_infos_backup")
|
||||
}
|
||||
|
||||
func v0_0_5a(db *gorm.DB) {
|
||||
log.Println("[>0.0.5a] Renaming column 'allow_cros' to 'allow_cors' in config table...")
|
||||
db.Migrator().RenameColumn(&conf.V1Struct{}, "allow_cros", "allow_cors")
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func v1_0_2_Oidc(db *gorm.DB) {
|
||||
log.Println("[>1.0.2] Merge OidcProvider table....")
|
||||
var config struct {
|
||||
OAuthClientID string `json:"o_auth_client_id" gorm:"type:varchar(255)"`
|
||||
OAuthClientSecret string `json:"o_auth_client_secret" gorm:"type:varchar(255)"`
|
||||
}
|
||||
if err := db.Raw("SELECT * FROM configs LIMIT 1").Scan(&config).Error; err != nil {
|
||||
log.Println("Failed to get config for OIDC provider migration:", err)
|
||||
}
|
||||
db.AutoMigrate(&models.OidcProvider{})
|
||||
j, err := json.Marshal(&map[string]string{
|
||||
"client_id": config.OAuthClientID,
|
||||
"client_secret": config.OAuthClientSecret,
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("Failed to marshal OIDC provider config:", err)
|
||||
return
|
||||
}
|
||||
db.Save(&models.OidcProvider{
|
||||
Name: "github",
|
||||
Addition: string(j),
|
||||
})
|
||||
db.AutoMigrate(&conf.V1Struct{})
|
||||
db.Model(&conf.V1Struct{}).Where("id = 1").Update("o_auth_provider", "github")
|
||||
}
|
||||
|
||||
func v1_0_2_MessageSender(db *gorm.DB) {
|
||||
log.Println("[>1.0.2] Migrate MessageSender configuration....")
|
||||
var config struct {
|
||||
TelegramBotToken string `json:"telegram_bot_token" gorm:"type:varchar(255)"`
|
||||
TelegramChatID string `json:"telegram_chat_id" gorm:"type:varchar(255)"`
|
||||
TelegramEndpoint string `json:"telegram_endpoint" gorm:"type:varchar(255)"`
|
||||
EmailHost string `json:"email_host" gorm:"type:varchar(255)"`
|
||||
EmailPort int `json:"email_port" gorm:"type:int"`
|
||||
EmailUsername string `json:"email_username" gorm:"type:varchar(255)"`
|
||||
EmailPassword string `json:"email_password" gorm:"type:varchar(255)"`
|
||||
EmailSender string `json:"email_sender" gorm:"type:varchar(255)"`
|
||||
EmailReceiver string `json:"email_receiver" gorm:"type:varchar(255)"`
|
||||
EmailUseSSL bool `json:"email_use_ssl" gorm:"type:boolean"`
|
||||
NotificationMethod string `json:"notification_method" gorm:"type:varchar(50)"`
|
||||
}
|
||||
if err := db.Raw("SELECT * FROM configs LIMIT 1").Scan(&config).Error; err != nil {
|
||||
log.Println("Failed to get config for MessageSender migration:", err)
|
||||
}
|
||||
|
||||
db.AutoMigrate(&models.MessageSenderProvider{})
|
||||
|
||||
// 迁移Telegram配置
|
||||
if config.NotificationMethod == "telegram" && config.TelegramBotToken != "" {
|
||||
telegramConfig := map[string]interface{}{
|
||||
"bot_token": config.TelegramBotToken,
|
||||
"chat_id": config.TelegramChatID,
|
||||
"endpoint": config.TelegramEndpoint,
|
||||
}
|
||||
if telegramConfig["endpoint"] == "" {
|
||||
telegramConfig["endpoint"] = "https://api.telegram.org/bot"
|
||||
}
|
||||
telegramConfigJSON, err := json.Marshal(telegramConfig)
|
||||
if err != nil {
|
||||
log.Println("Failed to marshal Telegram config:", err)
|
||||
} else {
|
||||
db.Save(&models.MessageSenderProvider{
|
||||
Name: "telegram",
|
||||
Addition: string(telegramConfigJSON),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 迁移Email配置
|
||||
if config.NotificationMethod == "email" && config.EmailHost != "" {
|
||||
emailConfig := map[string]interface{}{
|
||||
"host": config.EmailHost,
|
||||
"port": config.EmailPort,
|
||||
"username": config.EmailUsername,
|
||||
"password": config.EmailPassword,
|
||||
"sender": config.EmailSender,
|
||||
"receiver": config.EmailReceiver,
|
||||
"use_ssl": config.EmailUseSSL,
|
||||
}
|
||||
emailConfigJSON, err := json.Marshal(emailConfig)
|
||||
if err != nil {
|
||||
log.Println("Failed to marshal Email config:", err)
|
||||
} else {
|
||||
db.Save(&models.MessageSenderProvider{
|
||||
Name: "email",
|
||||
Addition: string(emailConfigJSON),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 删除旧的配置字段
|
||||
if db.Migrator().HasColumn(&conf.V1Struct{}, "telegram_bot_token") {
|
||||
db.Migrator().DropColumn(&conf.V1Struct{}, "telegram_bot_token")
|
||||
}
|
||||
if db.Migrator().HasColumn(&conf.V1Struct{}, "telegram_chat_id") {
|
||||
db.Migrator().DropColumn(&conf.V1Struct{}, "telegram_chat_id")
|
||||
}
|
||||
if db.Migrator().HasColumn(&conf.V1Struct{}, "telegram_endpoint") {
|
||||
db.Migrator().DropColumn(&conf.V1Struct{}, "telegram_endpoint")
|
||||
}
|
||||
if db.Migrator().HasColumn(&conf.V1Struct{}, "email_host") {
|
||||
db.Migrator().DropColumn(&conf.V1Struct{}, "email_host")
|
||||
}
|
||||
if db.Migrator().HasColumn(&conf.V1Struct{}, "email_port") {
|
||||
db.Migrator().DropColumn(&conf.V1Struct{}, "email_port")
|
||||
}
|
||||
if db.Migrator().HasColumn(&conf.V1Struct{}, "email_username") {
|
||||
db.Migrator().DropColumn(&conf.V1Struct{}, "email_username")
|
||||
}
|
||||
if db.Migrator().HasColumn(&conf.V1Struct{}, "email_password") {
|
||||
db.Migrator().DropColumn(&conf.V1Struct{}, "email_password")
|
||||
}
|
||||
if db.Migrator().HasColumn(&conf.V1Struct{}, "email_sender") {
|
||||
db.Migrator().DropColumn(&conf.V1Struct{}, "email_sender")
|
||||
}
|
||||
if db.Migrator().HasColumn(&conf.V1Struct{}, "email_receiver") {
|
||||
db.Migrator().DropColumn(&conf.V1Struct{}, "email_receiver")
|
||||
}
|
||||
if db.Migrator().HasColumn(&conf.V1Struct{}, "email_use_ssl") {
|
||||
db.Migrator().DropColumn(&conf.V1Struct{}, "email_use_ssl")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func v1_1_4(db *gorm.DB) {
|
||||
slog.Info("[>1.1.4] Migrating config table to file config...")
|
||||
var old_config struct {
|
||||
ID uint `json:"id,omitempty" gorm:"primaryKey;autoIncrement"` // 1
|
||||
Sitename string `json:"sitename" gorm:"type:varchar(100);not null"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
AllowCors bool `json:"allow_cors" gorm:"column:allow_cors;default:false"`
|
||||
Theme string `json:"theme" gorm:"type:varchar(100);default:'default'"` // 主题名称,默认 'default'
|
||||
PrivateSite bool `json:"private_site" gorm:"default:false"` // 是否为私有站点,默认 false
|
||||
ApiKey string `json:"api_key" gorm:"type:varchar(255);default:''"`
|
||||
AutoDiscoveryKey string `json:"auto_discovery_key" gorm:"type:varchar(255);default:''"` // 自动发现密钥
|
||||
ScriptDomain string `json:"script_domain" gorm:"type:varchar(255);default:''"` // 自定义脚本域名
|
||||
SendIpAddrToGuest bool `json:"send_ip_addr_to_guest" gorm:"default:false"` // 是否向访客页面发送 IP 地址,默认 false
|
||||
EulaAccepted bool `json:"eula_accepted" gorm:"default:false"`
|
||||
// GeoIP 配置
|
||||
GeoIpEnabled bool `json:"geo_ip_enabled" gorm:"default:true"`
|
||||
GeoIpProvider string `json:"geo_ip_provider" gorm:"type:varchar(20);default:'ip-api'"` // empty, mmdb, ip-api, geojs
|
||||
// Nezha 兼容(Agent gRPC)
|
||||
NezhaCompatEnabled bool `json:"nezha_compat_enabled" gorm:"default:false"`
|
||||
NezhaCompatListen string `json:"nezha_compat_listen" gorm:"type:varchar(100);default:''"` // 例如 0.0.0.0:5555
|
||||
// OAuth 配置
|
||||
OAuthEnabled bool `json:"o_auth_enabled" gorm:"default:false"`
|
||||
OAuthProvider string `json:"o_auth_provider" gorm:"type:varchar(50);default:'github'"`
|
||||
DisablePasswordLogin bool `json:"disable_password_login" gorm:"default:false"`
|
||||
// 自定义美化
|
||||
CustomHead string `json:"custom_head" gorm:"type:longtext"`
|
||||
CustomBody string `json:"custom_body" gorm:"type:longtext"`
|
||||
// 通知
|
||||
NotificationEnabled bool `json:"notification_enabled" gorm:"default:false"` // 通知总开关
|
||||
NotificationMethod string `json:"notification_method" gorm:"type:varchar(64);default:'none'"`
|
||||
NotificationTemplate string `json:"notification_template" gorm:"type:longtext;default:'{{emoji}}{{emoji}}{{emoji}}\nEvent: {{event}}\nClients: {{client}}\nMessage: {{message}}\nTime: {{time}}'"`
|
||||
ExpireNotificationEnabled bool `json:"expire_notification_enabled" gorm:"default:false"` // 是否启用过期通知
|
||||
ExpireNotificationLeadDays int `json:"expire_notification_lead_days" gorm:"default:7"` // 过期前多少天通知,默认7天
|
||||
LoginNotification bool `json:"login_notification" gorm:"default:false"` // 登录通知
|
||||
TrafficLimitPercentage float64 `json:"traffic_limit_percentage" gorm:"default:80.00"` // 流量限制百分比,默认80.00%
|
||||
// Record
|
||||
RecordEnabled bool `json:"record_enabled" gorm:"default:true"` // 是否启用记录功能
|
||||
RecordPreserveTime int `json:"record_preserve_time" gorm:"default:720"` // 记录保留时间,单位小时,默认30天
|
||||
PingRecordPreserveTime int `json:"ping_record_preserve_time" gorm:"default:24"` // Ping 记录保留时间,单位小时,默认1天
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
if err := db.Raw("SELECT * FROM configs LIMIT 1").Scan(&old_config).Error; err != nil {
|
||||
slog.Error("Failed to get config.", slog.Any("error", err))
|
||||
return
|
||||
}
|
||||
// 将旧结构转为 map[string]interface{},键为扁平 json 标签
|
||||
bytes, err := json.Marshal(&old_config)
|
||||
if err != nil {
|
||||
slog.Error("Failed to marshal old config.", slog.Any("error", err))
|
||||
return
|
||||
}
|
||||
var flat map[string]interface{}
|
||||
if err := json.Unmarshal(bytes, &flat); err != nil {
|
||||
slog.Error("Failed to unmarshal to map.", slog.Any("error", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 使用 conf.Update 以 SavePartial 语义合并,并兼容旧键名
|
||||
if err := conf.Update(flat); err != nil {
|
||||
slog.Error("Failed to write new file config.", slog.Any("error", err))
|
||||
return
|
||||
}
|
||||
db.Migrator().DropTable(&conf.V1Struct{})
|
||||
slog.Info("[>1.1.4] Config migration finished.")
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package restore
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var backupZipPath = filepath.Join(".", "data", "backup.zip")
|
||||
|
||||
func NeedBackupRestore() bool {
|
||||
if _, statErr := os.Stat(backupZipPath); statErr == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func RestoreBackup() {
|
||||
// 4. 把除了 ./data/backup.zip 之外的所有文件压缩到 ./backup/{time}.zip
|
||||
if err := os.MkdirAll("./backup", 0755); err != nil {
|
||||
log.Printf("[restore] failed to create backup dir: %v", err)
|
||||
} else {
|
||||
tsName := time.Now().Format("20060102-150405")
|
||||
bakPath := filepath.Join("./backup", fmt.Sprintf("%s.zip", tsName))
|
||||
if zipErr := zipDirectoryExcluding("./data", bakPath, map[string]struct{}{backupZipPath: {}}); zipErr != nil {
|
||||
log.Printf("[restore] failed to zip current data: %v", zipErr)
|
||||
} else {
|
||||
log.Printf("[restore] current data zipped to %s", bakPath)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 删除除了 ./data/backup.zip 之外的所有文件
|
||||
if delErr := removeAllInDirExcept("./data", map[string]struct{}{backupZipPath: {}}); delErr != nil {
|
||||
log.Printf("[restore] failed to cleanup data dir: %v", delErr)
|
||||
}
|
||||
|
||||
// 6. 解压 ./data/backup.zip 到 ./data
|
||||
if unzipErr := unzipToDir(backupZipPath, "./data"); unzipErr != nil {
|
||||
log.Printf("[restore] failed to unzip backup into data: %v", unzipErr)
|
||||
} else {
|
||||
log.Printf("[restore] backup.zip extracted to ./data")
|
||||
}
|
||||
|
||||
// 7. 删除 ./data/backup.zip
|
||||
if rmErr := os.Remove(backupZipPath); rmErr != nil {
|
||||
log.Printf("[restore] failed to remove backup.zip: %v", rmErr)
|
||||
} else {
|
||||
log.Printf("[restore] backup.zip removed")
|
||||
}
|
||||
// 8. 删除标记
|
||||
if rmErr := os.Remove("./data/komari-backup-markup"); rmErr != nil {
|
||||
log.Printf("[restore] failed to remove komari-backup-markup: %v", rmErr)
|
||||
} else {
|
||||
log.Printf("[restore] komari-backup-markup removed")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// zipDirectoryExcluding 将 srcDir 打包为 dstZip,exclude 是绝对路径集合需要排除
|
||||
func zipDirectoryExcluding(srcDir, dstZip string, exclude map[string]struct{}) error {
|
||||
// 规范化排除路径为绝对路径
|
||||
normExclude := make(map[string]struct{}, len(exclude))
|
||||
for p := range exclude {
|
||||
abs, _ := filepath.Abs(p)
|
||||
normExclude[abs] = struct{}{}
|
||||
}
|
||||
|
||||
out, err := os.Create(dstZip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
zw := zip.NewWriter(out)
|
||||
defer zw.Close()
|
||||
|
||||
absSrc, _ := filepath.Abs(srcDir)
|
||||
walkErr := filepath.Walk(absSrc, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 排除 backup.zip 本身
|
||||
if _, ok := normExclude[path]; ok {
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// 计算 zip 内相对路径
|
||||
rel, err := filepath.Rel(absSrc, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 根目录跳过
|
||||
if rel == "." {
|
||||
return nil
|
||||
}
|
||||
// 替换为正斜杠
|
||||
zipName := filepath.ToSlash(rel)
|
||||
|
||||
if info.IsDir() {
|
||||
_, err := zw.Create(zipName + "/")
|
||||
return err
|
||||
}
|
||||
// 普通文件
|
||||
fh, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w, err := zw.Create(zipName)
|
||||
if err != nil {
|
||||
fh.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(w, fh); err != nil {
|
||||
fh.Close()
|
||||
return err
|
||||
}
|
||||
fh.Close()
|
||||
return nil
|
||||
})
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
return zw.Close()
|
||||
}
|
||||
|
||||
// removeAllInDirExcept 删除 dir 下除 exclude 指定绝对路径外的所有文件和文件夹
|
||||
func removeAllInDirExcept(dir string, exclude map[string]struct{}) error {
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
normExclude := make(map[string]struct{}, len(exclude))
|
||||
for p := range exclude {
|
||||
abs, _ := filepath.Abs(p)
|
||||
normExclude[abs] = struct{}{}
|
||||
}
|
||||
entries, err := os.ReadDir(absDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, e := range entries {
|
||||
full := filepath.Join(absDir, e.Name())
|
||||
if _, ok := normExclude[full]; ok {
|
||||
continue
|
||||
}
|
||||
if err := os.RemoveAll(full); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// unzipToDir 将 zipPath 解压到 dstDir,包含路径遍历保护
|
||||
func unzipToDir(zipPath, dstDir string) error {
|
||||
zr, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer zr.Close()
|
||||
|
||||
if err := os.MkdirAll(dstDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
absDst, _ := filepath.Abs(dstDir)
|
||||
|
||||
for _, f := range zr.File {
|
||||
// 构造目标路径并做路径遍历保护
|
||||
cleanName := filepath.Clean(f.Name)
|
||||
targetPath := filepath.Join(absDst, cleanName)
|
||||
if !strings.HasPrefix(targetPath, absDst+string(os.PathSeparator)) && targetPath != absDst {
|
||||
return fmt.Errorf("illegal file path in zip: %s", f.Name)
|
||||
}
|
||||
if f.FileInfo().IsDir() {
|
||||
if err := os.MkdirAll(targetPath, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
rc.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(out, rc); err != nil {
|
||||
out.Close()
|
||||
rc.Close()
|
||||
return err
|
||||
}
|
||||
out.Close()
|
||||
rc.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package version
|
||||
|
||||
var (
|
||||
CurrentVersion = "0.0.1"
|
||||
VersionHash = "unknown"
|
||||
)
|
||||
@@ -5,18 +5,18 @@ import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/komari-monitor/komari/cmd"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
logutil "github.com/komari-monitor/komari/internal/log"
|
||||
"github.com/komari-monitor/komari/internal/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if version.VersionHash == "unknown" {
|
||||
if conf.Version == conf.Version_Development {
|
||||
logutil.SetupGlobalLogger(slog.LevelDebug)
|
||||
} else {
|
||||
logutil.SetupGlobalLogger(slog.LevelInfo)
|
||||
}
|
||||
|
||||
log.Printf("Komari Monitor %s (hash: %s)", version.CurrentVersion, version.VersionHash)
|
||||
log.Printf("Komari Monitor %s (hash: %s)", conf.Version, conf.CommitHash)
|
||||
|
||||
cmd.Execute()
|
||||
}
|
||||
|
||||
+5
-6
@@ -14,8 +14,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
)
|
||||
|
||||
//go:embed dist
|
||||
@@ -54,12 +53,12 @@ func initIndex() {
|
||||
}
|
||||
RawIndexFile = string(index)
|
||||
}
|
||||
func UpdateIndex(cfg models.Config) {
|
||||
func UpdateIndex(cfg conf.V1Struct) {
|
||||
IndexFile = applyCustomizations(RawIndexFile, cfg)
|
||||
}
|
||||
|
||||
// applyCustomizations 应用自定义内容到HTML字符串
|
||||
func applyCustomizations(htmlContent string, cfg models.Config) string {
|
||||
func applyCustomizations(htmlContent string, cfg conf.V1Struct) string {
|
||||
var titleReplacement string
|
||||
if cfg.Sitename == "Komari" {
|
||||
titleReplacement = "<title>Komari Monitor</title>"
|
||||
@@ -144,7 +143,7 @@ func Static(r *gin.RouterGroup, noRoute func(handlers ...gin.HandlerFunc)) {
|
||||
}
|
||||
|
||||
// 获取当前主题配置
|
||||
cfg, err := config.Get()
|
||||
cfg, err := conf.GetWithV1Format()
|
||||
if err != nil || cfg.Theme == "default" || cfg.Theme == "" {
|
||||
// 使用默认主题(embedded文件)
|
||||
serveFromEmbedded(c, path)
|
||||
@@ -269,7 +268,7 @@ func serveThemeIndexWithCustomizations(c *gin.Context, indexPath string) {
|
||||
}
|
||||
|
||||
// 获取配置以应用自定义内容
|
||||
cfg, err := config.Get()
|
||||
cfg, err := conf.GetWithV1Format()
|
||||
if err != nil {
|
||||
// 如果获取配置失败,直接返回原始文件
|
||||
c.Header("Content-Type", "text/html")
|
||||
|
||||
+8
-8
@@ -17,8 +17,8 @@ import (
|
||||
"github.com/gookit/event"
|
||||
apiClient "github.com/komari-monitor/komari/internal/api_v1/client"
|
||||
"github.com/komari-monitor/komari/internal/common"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/database/auditlog"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/database/dbcore"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"github.com/komari-monitor/komari/internal/eventType"
|
||||
@@ -35,11 +35,11 @@ import (
|
||||
|
||||
func StartNezhaGRPCServer(listen string) {
|
||||
event.On(eventType.ConfigUpdated, event.ListenerFunc(func(e event.Event) error {
|
||||
New := e.Get("new").(models.Config)
|
||||
Old := e.Get("old").(models.Config)
|
||||
if New.NezhaCompatEnabled != Old.NezhaCompatEnabled {
|
||||
if New.NezhaCompatEnabled {
|
||||
if err := StartNezhaCompat(New.NezhaCompatListen); err != nil {
|
||||
New := e.Get("new").(conf.Config)
|
||||
Old := e.Get("old").(conf.Config)
|
||||
if New.Compact.Nezha.NezhaCompatEnabled != Old.Compact.Nezha.NezhaCompatEnabled {
|
||||
if New.Compact.Nezha.NezhaCompatEnabled {
|
||||
if err := StartNezhaCompat(New.Compact.Nezha.NezhaCompatListen); err != nil {
|
||||
log.Printf("start Nezha compat server error: %v", err)
|
||||
auditlog.EventLog("error", fmt.Sprintf("start Nezha compat server error: %v", err))
|
||||
}
|
||||
@@ -387,7 +387,7 @@ func (s *nezhaCompatServer) ReportGeoIP(ctx context.Context, in *proto.GeoIP) (*
|
||||
if in != nil && in.Ip != nil {
|
||||
if v4 := strings.TrimSpace(in.Ip.Ipv4); v4 != "" {
|
||||
updates["ipv4"] = v4
|
||||
if cfg, err := config.Get(); err == nil && cfg.GeoIpEnabled {
|
||||
if cfg, err := conf.GetWithV1Format(); err == nil && cfg.GeoIpEnabled {
|
||||
if ip := net.ParseIP(v4); ip != nil {
|
||||
if gi, _ := geoip.GetGeoInfo(ip); gi != nil {
|
||||
iso = gi.ISOCode
|
||||
@@ -398,7 +398,7 @@ func (s *nezhaCompatServer) ReportGeoIP(ctx context.Context, in *proto.GeoIP) (*
|
||||
if v6 := strings.TrimSpace(in.Ip.Ipv6); v6 != "" {
|
||||
updates["ipv6"] = v6
|
||||
if iso == "" { // 优先使用 v4 的国家码
|
||||
if cfg, err := config.Get(); err == nil && cfg.GeoIpEnabled {
|
||||
if cfg, err := conf.GetWithV1Format(); err == nil && cfg.GeoIpEnabled {
|
||||
if ip := net.ParseIP(v6); ip != nil {
|
||||
if gi, _ := geoip.GetGeoInfo(ip); gi != nil {
|
||||
iso = gi.ISOCode
|
||||
|
||||
+11
-12
@@ -8,8 +8,7 @@ import (
|
||||
"github.com/gookit/event"
|
||||
"github.com/komari-monitor/komari/internal"
|
||||
"github.com/komari-monitor/komari/internal/api_rpc"
|
||||
"github.com/komari-monitor/komari/internal/database/config"
|
||||
"github.com/komari-monitor/komari/internal/database/models"
|
||||
"github.com/komari-monitor/komari/internal/conf"
|
||||
"github.com/komari-monitor/komari/internal/eventType"
|
||||
"github.com/komari-monitor/komari/internal/geoip"
|
||||
"github.com/komari-monitor/komari/internal/messageSender"
|
||||
@@ -21,18 +20,18 @@ var (
|
||||
)
|
||||
|
||||
func Init(r *gin.Engine) {
|
||||
conf, _ := config.Get()
|
||||
AllowCors = conf.AllowCors
|
||||
config, _ := conf.GetWithV1Format()
|
||||
AllowCors = config.AllowCors
|
||||
|
||||
event.On(eventType.ConfigUpdated, event.ListenerFunc(func(e event.Event) error {
|
||||
newConf := e.Get("new").(models.Config)
|
||||
oldConf := e.Get("old").(models.Config)
|
||||
AllowCors = newConf.AllowCors
|
||||
public.UpdateIndex(newConf)
|
||||
if newConf.GeoIpProvider != oldConf.GeoIpProvider {
|
||||
newConf := e.Get("new").(conf.Config)
|
||||
oldConf := e.Get("old").(conf.Config)
|
||||
AllowCors = newConf.Site.AllowCors
|
||||
public.UpdateIndex(newConf.ToV1Format())
|
||||
if newConf.GeoIp.GeoIpProvider != oldConf.GeoIp.GeoIpProvider {
|
||||
go geoip.InitGeoIp()
|
||||
}
|
||||
if newConf.NotificationMethod != oldConf.NotificationMethod {
|
||||
if newConf.Notification.NotificationMethod != oldConf.Notification.NotificationMethod {
|
||||
go messageSender.Initialize()
|
||||
}
|
||||
return nil
|
||||
@@ -58,9 +57,9 @@ func Init(r *gin.Engine) {
|
||||
r.NoRoute(handlers...)
|
||||
})
|
||||
// #region 静态文件服务
|
||||
public.UpdateIndex(conf)
|
||||
public.UpdateIndex(config)
|
||||
|
||||
internal.LoadApiV1Routes(r, conf)
|
||||
internal.LoadApiV1Routes(r, config)
|
||||
|
||||
api_rpc.RegisterRouters("/api/rpc2", r)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user