This commit is contained in:
2026-08-16 01:19:10 +08:00
parent c4b114762b
commit c554152f4f
43 changed files with 1311 additions and 318 deletions
+1 -1
View File
@@ -94,7 +94,7 @@ func (r *announcementRepo) UserOptions(ctx context.Context) ([]biz.UserOption, e
Label string
Value uint
}
err := r.data.gormDB.WithContext(ctx).Table("sys_users").Select("nick_name AS label, id AS value").Where("deleted_at IS NULL").Scan(&rows).Error
err := r.data.gormDB.WithContext(ctx).Table("sys_users").Select("nick_name AS label, id AS value").Scan(&rows).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
+16 -9
View File
@@ -98,7 +98,7 @@ func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *biz.Au
return err
}
}
defaults := []struct{ path, method string }{{"/menu/getMenu", "POST"}, {"/jwt/jsonInBlacklist", "POST"}, {"/user/changePassword", "POST"}, {"/user/setUserAuthority", "POST"}, {"/user/getUserInfo", "GET"}, {"/user/setSelfInfo", "PUT"}, {"/fileUploadAndDownload/upload", "POST"}, {"/sysDictionary/findSysDictionary", "GET"}}
defaults := []struct{ path, method string }{{"/menu/getMenu", "POST"}, {"/jwt/jsonInBlacklist", "POST"}, {"/base/login", "POST"}, {"/user/changePassword", "POST"}, {"/user/setUserAuthority", "POST"}, {"/user/getUserInfo", "GET"}, {"/user/setSelfInfo", "PUT"}, {"/fileUploadAndDownload/upload", "POST"}, {"/sysDictionary/findSysDictionary", "GET"}}
for _, item := range defaults {
var api apiPO
if err := tx.Where("path = ? AND method = ?", item.path, item.method).First(&api).Error; err == nil {
@@ -186,13 +186,23 @@ func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint,
})
}
func (r *authorityAccessRepo) UpdateAuthority(ctx context.Context, value *biz.Authority) error {
return r.data.gormDB.WithContext(ctx).Model(&authorityPO{}).Where("authority_id = ?", value.AuthorityID).Updates(map[string]any{"authority_name": value.AuthorityName, "parent_id": value.ParentID, "data_scope": value.DataScope, "default_router": value.DefaultRouter}).Error
db := r.data.gormDB.WithContext(ctx)
var current authorityPO
if err := db.Where("authority_id = ?", value.AuthorityID).First(&current).Error; err != nil {
return errors.New("查询角色数据失败")
}
updates := &authorityPO{AuthorityName: value.AuthorityName, ParentID: value.ParentID, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}
return db.Model(&current).Updates(updates).Error
}
func (r *authorityAccessRepo) DeleteAuthority(ctx context.Context, id uint) error {
if id == 888 {
return errors.New("不能删除超级管理员")
}
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var authority authorityPO
if err := tx.Where("authority_id = ?", id).First(&authority).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("该角色不存在")
}
return err
}
var users, children int64
if err := tx.Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Count(&users).Error; err != nil {
return err
@@ -221,15 +231,12 @@ func (r *authorityAccessRepo) DeleteAuthority(ctx context.Context, id uint) erro
if err := tx.Where("authority_id = ?", id).Delete(&authorityButtonPO{}).Error; err != nil {
return err
}
if err := tx.Where("authority_id = ?", id).Delete(&authorityDepartmentPO{}).Error; err != nil {
return err
}
return tx.Delete(&authorityPO{}, "authority_id = ?", id).Error
})
}
func (r *authorityAccessRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) {
var pos []authorityPO
if err := r.data.gormDB.WithContext(ctx).Order("authority_id").Find(&pos).Error; err != nil {
if err := r.data.gormDB.WithContext(ctx).Find(&pos).Error; err != nil {
return nil, err
}
var allowed map[uint]bool
+31 -1
View File
@@ -66,6 +66,16 @@ func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) {
}
}
maskDataSecrets(dataConfig)
safeAdmin := cloneAdminConfig(adminConfig)
if safeAdmin != nil {
if safeAdmin.Jwt != nil {
safeAdmin.Jwt.SigningKey = "******"
}
if safeAdmin.Email != nil {
safeAdmin.Email.Secret = "******"
}
maskStorageSecrets(safeAdmin.Storage)
}
dataMap := map[string]any{}
if dataConfig != nil {
raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(dataConfig)
@@ -76,7 +86,9 @@ func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) {
return nil, err
}
}
return json.Marshal(map[string]any{"config": map[string]any{"admin": admin, "email": email, "data": dataMap}})
config := gvaConfiguration(dataConfig, safeAdmin)
config["admin"], config["email"], config["data"] = admin, email, dataMap
return json.Marshal(map[string]any{"config": config})
}
func (r *initializationRepo) SaveConfigurationJSON(ctx context.Context, raw json.RawMessage) error {
@@ -86,6 +98,9 @@ func (r *initializationRepo) SaveConfigurationJSON(ctx context.Context, raw json
}
nextData := cloneDataConfig(currentData)
nextAdmin := proto.Clone(currentAdmin).(*conf.AdminBackend)
if err := applyGVAConfiguration(raw, nextData, nextAdmin); err != nil {
return err
}
var value configurationEnvelope
if err := json.Unmarshal(raw, &value); err != nil {
return err
@@ -154,6 +169,13 @@ func cloneDataConfig(value *conf.Data) *conf.Data {
return proto.Clone(value).(*conf.Data)
}
func cloneAdminConfig(value *conf.AdminBackend) *conf.AdminBackend {
if value == nil {
return &conf.AdminBackend{}
}
return proto.Clone(value).(*conf.AdminBackend)
}
func refreshDatabaseSources(value *conf.Data) error {
if value == nil {
return nil
@@ -222,6 +244,11 @@ func applyDataPatch(target, patch *conf.Data) {
func applyAdminPatch(target, patch *conf.AdminBackend) {
target.RouterPrefix = patch.RouterPrefix
if patch.System != nil {
if target.System != nil {
patch.System.Addr = target.System.Addr
patch.System.IplimitCount = target.System.IplimitCount
patch.System.IplimitTime = target.System.IplimitTime
}
target.System = patch.System
}
if patch.Jwt != nil {
@@ -234,6 +261,9 @@ func applyAdminPatch(target, patch *conf.AdminBackend) {
target.Local = patch.Local
}
if patch.Media != nil {
if patch.Media.ChunkDir == "" && target.Media != nil {
patch.Media.ChunkDir = target.Media.ChunkDir
}
target.Media = patch.Media
}
if patch.Storage != nil {
+46
View File
@@ -4,8 +4,10 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"strconv"
"kra/internal/conf"
@@ -84,6 +86,45 @@ func mergeYAMLNode(dst, src *yaml.Node) {
dst.HeadComment, dst.LineComment, dst.FootComment = head, line, foot
}
func yamlMappingValue(node *yaml.Node, keys ...string) *yaml.Node {
if node == nil {
return nil
}
if node.Kind == yaml.DocumentNode && len(node.Content) > 0 {
node = node.Content[0]
}
for _, key := range keys {
if node.Kind != yaml.MappingNode {
return nil
}
var next *yaml.Node
for i := 0; i+1 < len(node.Content); i += 2 {
if node.Content[i].Value == key {
next = node.Content[i+1]
break
}
}
if next == nil {
return nil
}
node = next
}
return node
}
func setServerHTTPPort(document *yaml.Node, port int32) error {
if port <= 0 {
return nil
}
host := "0.0.0.0"
if addr := yamlMappingValue(document, "server", "http", "addr"); addr != nil {
if currentHost, _, err := net.SplitHostPort(addr.Value); err == nil && currentHost != "" {
host = currentHost
}
}
return setYAMLMapping(document, "server", map[string]any{"http": map[string]any{"addr": net.JoinHostPort(host, strconv.Itoa(int(port)))}})
}
func (d *Data) persistConfig() error {
dataConfig, adminConfig := d.runtime.Values()
return d.persistConfigValues(dataConfig, adminConfig)
@@ -122,6 +163,11 @@ func (d *Data) persistConfigValuesLocked(dataConfig *conf.Data, adminConfig *con
if err = setYAMLMapping(&document, "admin", adminValue); err != nil {
return err
}
if adminConfig.System != nil {
if err = setServerHTTPPort(&document, adminConfig.System.Addr); err != nil {
return err
}
}
return writeConfigDocument(configPath, &document)
}
+2 -8
View File
@@ -108,12 +108,6 @@ func (r *departmentRepo) DeleteDepartment(ctx context.Context, id uint) error {
if count > 0 {
return errors.New("存在子部门,不允许删除")
}
if err := r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("dept_id = ?", id).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New("该部门下存在用户,不允许删除")
}
if err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Count(&count).Error; err != nil {
return err
}
@@ -135,7 +129,7 @@ func (r *departmentRepo) FindDepartment(ctx context.Context, id uint) (*biz.Depa
}
func (r *departmentRepo) ListDepartments(ctx context.Context, name string) ([]*biz.Department, error) {
var pos []departmentPO
db := r.data.gormDB.WithContext(ctx).Order("sort,id")
db := r.data.gormDB.WithContext(ctx).Order("sort")
if name != "" {
db = db.Where("name LIKE ?", "%"+name+"%")
}
@@ -160,7 +154,7 @@ func (r *departmentRepo) ListDepartments(ctx context.Context, name string) ([]*b
n := nodes[po.ID]
if p := nodes[po.ParentID]; p != nil {
p.Children = append(p.Children, n)
} else {
} else if po.ParentID == 0 {
roots = append(roots, n)
}
}
+11 -12
View File
@@ -111,19 +111,18 @@ func (r *dictionaryRepo) UpdateDictionary(ctx context.Context, v *biz.Dictionary
return r.data.gormDB.WithContext(ctx).Model(&dictionaryPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "type": v.Type, "status": v.Status, "desc": v.Desc, "parent_id": v.ParentID}).Error
}
func (r *dictionaryRepo) DeleteDictionary(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var count int64
if err := tx.Model(&dictionaryPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil {
return err
db := r.data.gormDB.WithContext(ctx)
var dictionary dictionaryPO
if err := db.First(&dictionary, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("请不要搞事")
}
if count > 0 {
return errors.New("存在子字典不可删除")
}
if err := tx.Where("sys_dictionary_id = ?", id).Delete(&dictionaryDetailPO{}).Error; err != nil {
return err
}
return tx.Delete(&dictionaryPO{}, id).Error
})
return err
}
if err := db.Delete(&dictionary).Error; err != nil {
return err
}
return db.Where("sys_dictionary_id = ?", id).Delete(&dictionaryDetailPO{}).Error
}
func (r *dictionaryRepo) FindDictionary(ctx context.Context, id uint, typ string, status *bool, details bool) (*biz.Dictionary, error) {
var po dictionaryPO
+2 -38
View File
@@ -111,21 +111,7 @@ func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *biz.ExportTemp
})
}
func (r *exportRepo) DeleteExportTemplates(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var tids []string
if err := tx.Model(&exportTemplatePO{}).Where("id IN ?", ids).Pluck("template_id", &tids).Error; err != nil {
return err
}
if len(tids) > 0 {
if err := tx.Where("template_id IN ?", tids).Delete(&exportConditionPO{}).Error; err != nil {
return err
}
if err := tx.Where("template_id IN ?", tids).Delete(&exportJoinPO{}).Error; err != nil {
return err
}
}
return tx.Delete(&exportTemplatePO{}, ids).Error
})
return r.data.gormDB.WithContext(ctx).Delete(&[]exportTemplatePO{}, "id IN ?", ids).Error
}
func (r *exportRepo) FindExportTemplate(ctx context.Context, id uint, tid string) (*biz.ExportTemplate, error) {
var po exportTemplatePO
@@ -173,31 +159,9 @@ func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q
if err := applyPagination(db, page, size, 0).Find(&pos).Error; err != nil {
return nil, 0, err
}
templateIDs := make([]string, 0, len(pos))
for _, po := range pos {
templateIDs = append(templateIDs, po.TemplateID)
}
var conditions []exportConditionPO
var joins []exportJoinPO
if len(templateIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("template_id IN ?", templateIDs).Find(&conditions).Error; err != nil {
return nil, 0, err
}
if err := r.data.gormDB.WithContext(ctx).Where("template_id IN ?", templateIDs).Find(&joins).Error; err != nil {
return nil, 0, err
}
}
conditionsByTemplate := make(map[string][]exportConditionPO, len(pos))
for _, condition := range conditions {
conditionsByTemplate[condition.TemplateID] = append(conditionsByTemplate[condition.TemplateID], condition)
}
joinsByTemplate := make(map[string][]exportJoinPO, len(pos))
for _, join := range joins {
joinsByTemplate[join.TemplateID] = append(joinsByTemplate[join.TemplateID], join)
}
out := make([]*biz.ExportTemplate, 0, len(pos))
for _, po := range pos {
out = append(out, exportFromPO(po, conditionsByTemplate[po.TemplateID], joinsByTemplate[po.TemplateID]))
out = append(out, exportFromPO(po, nil, nil))
}
return out, total, nil
}
+545
View File
@@ -0,0 +1,545 @@
package data
import (
"encoding/json"
"strconv"
"strings"
"time"
"kra/internal/conf"
"google.golang.org/protobuf/types/known/durationpb"
)
func gvaDatabase(value *conf.Data_Database) map[string]any {
if value == nil {
return map[string]any{}
}
path := value.Host
if value.Driver == "sqlite" {
path = value.Path
}
return map[string]any{
"prefix": value.Prefix, "port": value.Port, "config": value.Config,
"db-name": value.Name, "username": value.User, "password": value.Password,
"path": path, "engine": value.Engine, "log-mode": value.LogMode,
"max-idle-conns": value.MaxIdleConns, "max-open-conns": value.MaxOpenConns,
"conn-max-lifetime": value.ConnMaxLifetime, "singular": value.Singular,
}
}
func gvaRedis(value *conf.Data_Redis) map[string]any {
if value == nil {
return map[string]any{}
}
return map[string]any{"name": value.Name, "addr": value.Addr, "password": value.Password, "db": value.Db, "useCluster": value.UseCluster, "clusterAddrs": value.ClusterAddrs}
}
func gvaObjectStore(value *conf.AdminBackend_ObjectStore) map[string]any {
if value == nil {
return map[string]any{}
}
return map[string]any{
"endpoint": value.Endpoint, "region": value.Region, "bucket": value.Bucket,
"access-key": value.AccessKey, "secret-key": value.SecretKey, "base-url": value.BaseUrl,
"path-prefix": value.PathPrefix, "use-ssl": value.UseSsl,
"s3-force-path-style": value.ForcePathStyle, "account-id": value.AccountId,
}
}
func gvaObjectStores(storage *conf.AdminBackend_Storage) map[string]map[string]any {
if storage == nil {
storage = &conf.AdminBackend_Storage{}
}
aliyun, huawei := storage.AliyunOss, storage.HuaweiObs
tencent, aws := storage.TencentCos, storage.AwsS3
cloudflare, minio := storage.CloudflareR2, storage.Minio
if aliyun == nil {
aliyun = &conf.AdminBackend_ObjectStore{}
}
if huawei == nil {
huawei = &conf.AdminBackend_ObjectStore{}
}
if tencent == nil {
tencent = &conf.AdminBackend_ObjectStore{}
}
if aws == nil {
aws = &conf.AdminBackend_ObjectStore{}
}
if cloudflare == nil {
cloudflare = &conf.AdminBackend_ObjectStore{}
}
if minio == nil {
minio = &conf.AdminBackend_ObjectStore{}
}
return map[string]map[string]any{
"aliyun-oss": {"endpoint": aliyun.Endpoint, "access-key-id": aliyun.AccessKey, "access-key-secret": aliyun.SecretKey, "bucket-name": aliyun.Bucket, "bucket-url": aliyun.BaseUrl, "base-path": aliyun.PathPrefix},
"hua-wei-obs": {"path": huawei.PathPrefix, "bucket": huawei.Bucket, "endpoint": huawei.Endpoint, "access-key": huawei.AccessKey, "secret-key": huawei.SecretKey},
"tencent-cos": {"bucket": tencent.Bucket, "region": tencent.Region, "secret-id": tencent.AccessKey, "secret-key": tencent.SecretKey, "base-url": tencent.BaseUrl, "path-prefix": tencent.PathPrefix},
"aws-s3": {"bucket": aws.Bucket, "region": aws.Region, "endpoint": aws.Endpoint, "secret-id": aws.AccessKey, "secret-key": aws.SecretKey, "base-url": aws.BaseUrl, "path-prefix": aws.PathPrefix, "s3-force-path-style": aws.ForcePathStyle, "disable-ssl": !aws.UseSsl},
"cloudflare-r2": {"bucket": cloudflare.Bucket, "base-url": cloudflare.BaseUrl, "path": cloudflare.PathPrefix, "account-id": cloudflare.AccountId, "access-key-id": cloudflare.AccessKey, "secret-access-key": cloudflare.SecretKey},
"minio": {"endpoint": minio.Endpoint, "access-key-id": minio.AccessKey, "access-key-secret": minio.SecretKey, "bucket-name": minio.Bucket, "use-ssl": minio.UseSsl, "base-path": minio.PathPrefix, "bucket-url": minio.BaseUrl},
}
}
func gvaConfiguration(data *conf.Data, admin *conf.AdminBackend) map[string]any {
result := map[string]any{}
if data == nil {
data = &conf.Data{}
}
if admin == nil {
admin = &conf.AdminBackend{}
}
jwt := admin.Jwt
if jwt == nil {
jwt = &conf.AdminBackend_JWT{}
}
result["jwt"] = map[string]any{"signing-key": jwt.SigningKey, "expires-time": durationString(jwt.ExpiresTime), "buffer-time": durationString(jwt.BufferTime), "issuer": jwt.Issuer}
zap := admin.Zap
if zap == nil {
zap = &conf.AdminBackend_Zap{}
}
result["zap"] = map[string]any{
"level": zap.Level, "prefix": zap.Prefix, "format": zap.Format, "director": zap.Director,
"encode-level": zap.EncodeLevel, "stacktrace-key": zap.StacktraceKey, "show-line": zap.ShowLine,
"log-in-console": zap.LogInConsole, "retention-day": zap.RetentionDay,
"access-req-body": zap.AccessReqBody, "access-resp-data": zap.AccessRespData,
"access-req-headers": zap.AccessReqHeaders, "access-log-max-bytes": zap.AccessLogMaxBytes,
"file-only-modules": zap.FileOnlyModules,
}
result["redis"] = gvaRedis(data.Redis)
redisList := make([]map[string]any, 0, len(data.RedisList))
for _, item := range data.RedisList {
redisList = append(redisList, gvaRedis(item))
}
result["redis-list"] = redisList
mongo := data.Mongo
if mongo == nil {
mongo = &conf.Data_Mongo{}
}
hosts := make([]map[string]any, 0, len(mongo.Hosts))
for _, host := range mongo.Hosts {
if host != nil {
hosts = append(hosts, map[string]any{"host": host.Host, "port": host.Port})
}
}
result["mongo"] = map[string]any{"coll": mongo.Coll, "options": mongo.Options, "database": mongo.Database, "username": mongo.Username, "password": mongo.Password, "auth-source": mongo.AuthSource, "min-pool-size": mongo.MinPoolSize, "max-pool-size": mongo.MaxPoolSize, "socket-timeout-ms": mongo.SocketTimeoutMs, "connect-timeout-ms": mongo.ConnectTimeoutMs, "is-zap": mongo.IsZap, "hosts": hosts}
email := admin.Email
if email == nil {
email = &conf.AdminBackend_Email{}
}
result["email"] = map[string]any{"to": email.To, "from": email.From, "host": email.Host, "secret": email.Secret, "nickname": email.Nickname, "port": email.Port, "is-ssl": email.IsSsl, "is-loginauth": email.IsLoginAuth}
system := admin.System
if system == nil {
system = &conf.AdminBackend_System{}
}
storageType := "local"
if admin.Storage != nil && admin.Storage.Type != "" {
storageType = admin.Storage.Type
}
driver := ""
if data.Database != nil {
driver = data.Database.Driver
}
result["system"] = map[string]any{"db-type": driver, "oss-type": storageType, "router-prefix": admin.RouterPrefix, "addr": system.Addr, "iplimit-count": system.IplimitCount, "iplimit-time": system.IplimitTime, "use-multipoint": system.UseMultipoint, "use-redis": system.UseRedis, "use-mongo": system.UseMongo, "use-strict-auth": system.UseStrictAuth, "disable-auto-migrate": system.DisableAutoMigrate}
for _, name := range []string{"mysql", "mssql", "pgsql", "oracle", "sqlite"} {
result[name] = map[string]any{}
}
if driver != "" {
result[driver] = gvaDatabase(data.Database)
}
dbList := make([]map[string]any, 0, len(data.DatabaseList))
for _, item := range data.DatabaseList {
row := gvaDatabase(item)
if item != nil {
row["type"], row["alias-name"], row["disable"] = item.Driver, item.AliasName, item.Disable
}
dbList = append(dbList, row)
}
result["db-list"] = dbList
local := admin.Local
if local == nil {
local = &conf.AdminBackend_Local{}
}
result["local"] = map[string]any{"path": local.PathPrefix, "store-path": local.StorePath}
media := admin.Media
if media == nil {
media = &conf.AdminBackend_Media{}
}
result["media"] = map[string]any{"session-ttl": media.SessionTtl, "max-file-size": media.MaxFileSize, "chunk-dir": media.ChunkDir}
if admin.Storage == nil {
admin.Storage = &conf.AdminBackend_Storage{}
}
qiniu := admin.Storage.Qiniu
if qiniu == nil {
qiniu = &conf.AdminBackend_Qiniu{}
}
result["qiniu"] = map[string]any{"zone": qiniu.Zone, "bucket": qiniu.Bucket, "img-path": qiniu.BaseUrl, "access-key": qiniu.AccessKey, "secret-key": qiniu.SecretKey, "use-https": qiniu.UseHttps, "use-cdn-domains": qiniu.UseCdnDomains}
for key, value := range gvaObjectStores(admin.Storage) {
result[key] = value
}
disks := make([]map[string]any, 0, len(admin.DiskList))
for _, disk := range admin.DiskList {
if disk != nil {
disks = append(disks, map[string]any{"mount-point": disk.MountPoint})
}
}
result["disk-list"] = disks
cors := admin.Cors
if cors == nil {
cors = &conf.AdminBackend_CORS{}
}
whitelist := make([]map[string]any, 0, len(cors.Whitelist))
for _, rule := range cors.Whitelist {
if rule != nil {
whitelist = append(whitelist, map[string]any{"allow-origin": rule.AllowOrigin, "allow-methods": rule.AllowMethods, "allow-headers": rule.AllowHeaders, "expose-headers": rule.ExposeHeaders, "allow-credentials": rule.AllowCredentials})
}
}
result["cors"] = map[string]any{"mode": cors.Mode, "whitelist": whitelist}
result["app"] = map[string]any{"node": admin.GetApp().GetNode(), "app-id": admin.GetApp().GetAppId(), "env": admin.GetApp().GetEnv()}
return result
}
func objectMap(value any) map[string]any {
result, _ := value.(map[string]any)
return result
}
func stringValue(values map[string]any, key string) string {
value, _ := values[key].(string)
return value
}
func boolValue(values map[string]any, key string, fallback bool) bool {
value, ok := values[key].(bool)
if !ok {
return fallback
}
return value
}
func int64Value(values map[string]any, key string, fallback int64) int64 {
value, ok := values[key].(float64)
if !ok {
return fallback
}
return int64(value)
}
func stringSliceValue(values map[string]any, key string, fallback []string) []string {
items, ok := values[key].([]any)
if !ok {
return fallback
}
result := make([]string, 0, len(items))
for _, item := range items {
if value, ok := item.(string); ok {
result = append(result, value)
}
}
return result
}
func applyGVARedis(target *conf.Data_Redis, values map[string]any) {
if target == nil || values == nil {
return
}
target.Name, target.Addr = stringValue(values, "name"), stringValue(values, "addr")
if password := stringValue(values, "password"); password != "" && password != "******" {
target.Password = password
}
target.Db = int32(int64Value(values, "db", int64(target.Db)))
target.UseCluster = boolValue(values, "useCluster", target.UseCluster)
target.ClusterAddrs = stringSliceValue(values, "clusterAddrs", target.ClusterAddrs)
}
func durationValue(raw string, fallback *durationpb.Duration) *durationpb.Duration {
if raw == "" {
return fallback
}
value, err := parseGVADuration(raw)
if err != nil {
return fallback
}
return durationpb.New(value)
}
func parseGVADuration(raw string) (time.Duration, error) {
raw = strings.TrimSpace(raw)
if value, err := time.ParseDuration(raw); err == nil {
return value, nil
}
if index := strings.Index(raw, "d"); index >= 0 {
days, _ := strconv.Atoi(raw[:index])
value := time.Duration(days) * 24 * time.Hour
remainder, err := time.ParseDuration(raw[index+1:])
if err != nil {
return value, nil
}
return value + remainder, nil
}
nanoseconds, err := strconv.ParseInt(raw, 10, 64)
return time.Duration(nanoseconds), err
}
func applyGVADatabase(target *conf.Data_Database, values map[string]any, driver string) {
if target == nil || values == nil {
return
}
target.Driver = driver
target.Prefix, target.Port, target.Config = stringValue(values, "prefix"), stringValue(values, "port"), stringValue(values, "config")
target.Name, target.User = stringValue(values, "db-name"), stringValue(values, "username")
if password := stringValue(values, "password"); password != "" && password != "******" {
target.Password = password
}
if driver == "sqlite" {
target.Path = stringValue(values, "path")
} else {
target.Host = stringValue(values, "path")
}
target.Engine, target.LogMode = stringValue(values, "engine"), stringValue(values, "log-mode")
target.MaxIdleConns = int32(int64Value(values, "max-idle-conns", int64(target.MaxIdleConns)))
target.MaxOpenConns = int32(int64Value(values, "max-open-conns", int64(target.MaxOpenConns)))
target.ConnMaxLifetime = int32(int64Value(values, "conn-max-lifetime", int64(target.ConnMaxLifetime)))
target.Singular = boolValue(values, "singular", target.Singular)
}
func applyGVAObjectStore(target *conf.AdminBackend_ObjectStore, values map[string]any) {
if target == nil || values == nil {
return
}
target.Endpoint, target.Region, target.Bucket = stringValue(values, "endpoint"), stringValue(values, "region"), stringValue(values, "bucket")
if target.Bucket == "" {
target.Bucket = stringValue(values, "bucket-name")
}
target.AccessKey = stringValue(values, "access-key")
if target.AccessKey == "" {
target.AccessKey = stringValue(values, "access-key-id")
}
if target.AccessKey == "" {
target.AccessKey = stringValue(values, "secret-id")
}
secret := stringValue(values, "secret-key")
if secret == "" {
secret = stringValue(values, "access-key-secret")
}
if secret == "" {
secret = stringValue(values, "secret-access-key")
}
if secret != "" && secret != "******" {
target.SecretKey = secret
}
target.BaseUrl = stringValue(values, "base-url")
if target.BaseUrl == "" {
target.BaseUrl = stringValue(values, "bucket-url")
}
target.PathPrefix = stringValue(values, "path-prefix")
if target.PathPrefix == "" {
target.PathPrefix = stringValue(values, "base-path")
}
if target.PathPrefix == "" {
target.PathPrefix = stringValue(values, "path")
}
target.UseSsl = boolValue(values, "use-ssl", !boolValue(values, "disable-ssl", !target.UseSsl))
target.ForcePathStyle = boolValue(values, "s3-force-path-style", target.ForcePathStyle)
target.AccountId = stringValue(values, "account-id")
}
func applyGVAConfiguration(raw json.RawMessage, data *conf.Data, admin *conf.AdminBackend) error {
var values map[string]any
if err := json.Unmarshal(raw, &values); err != nil {
return err
}
if _, isGVA := values["system"]; !isGVA {
return nil
}
if data.Database == nil {
data.Database = &conf.Data_Database{}
}
if admin.System == nil {
admin.System = &conf.AdminBackend_System{}
}
if admin.Storage == nil {
admin.Storage = &conf.AdminBackend_Storage{}
}
system := objectMap(values["system"])
driver := stringValue(system, "db-type")
if driver == "" {
driver = data.Database.Driver
}
applyGVADatabase(data.Database, objectMap(values[driver]), driver)
admin.Storage.Type = stringValue(system, "oss-type")
admin.RouterPrefix = stringValue(system, "router-prefix")
admin.System.UseMultipoint = boolValue(system, "use-multipoint", admin.System.UseMultipoint)
admin.System.UseRedis = boolValue(system, "use-redis", admin.System.UseRedis)
admin.System.UseMongo = boolValue(system, "use-mongo", admin.System.UseMongo)
admin.System.UseStrictAuth = boolValue(system, "use-strict-auth", admin.System.UseStrictAuth)
admin.System.DisableAutoMigrate = boolValue(system, "disable-auto-migrate", admin.System.DisableAutoMigrate)
admin.System.Addr = int32(int64Value(system, "addr", int64(admin.System.Addr)))
admin.System.IplimitCount = int32(int64Value(system, "iplimit-count", int64(admin.System.IplimitCount)))
admin.System.IplimitTime = int32(int64Value(system, "iplimit-time", int64(admin.System.IplimitTime)))
if admin.Jwt == nil {
admin.Jwt = &conf.AdminBackend_JWT{}
}
jwt := objectMap(values["jwt"])
if secret := stringValue(jwt, "signing-key"); secret != "" && secret != "******" {
admin.Jwt.SigningKey = secret
}
admin.Jwt.ExpiresTime = durationValue(stringValue(jwt, "expires-time"), admin.Jwt.ExpiresTime)
admin.Jwt.BufferTime = durationValue(stringValue(jwt, "buffer-time"), admin.Jwt.BufferTime)
admin.Jwt.Issuer = stringValue(jwt, "issuer")
if admin.Zap == nil {
admin.Zap = &conf.AdminBackend_Zap{}
}
zap := objectMap(values["zap"])
admin.Zap.Level, admin.Zap.Prefix, admin.Zap.Format, admin.Zap.Director = stringValue(zap, "level"), stringValue(zap, "prefix"), stringValue(zap, "format"), stringValue(zap, "director")
admin.Zap.EncodeLevel, admin.Zap.StacktraceKey = stringValue(zap, "encode-level"), stringValue(zap, "stacktrace-key")
admin.Zap.ShowLine, admin.Zap.LogInConsole = boolValue(zap, "show-line", admin.Zap.ShowLine), boolValue(zap, "log-in-console", admin.Zap.LogInConsole)
admin.Zap.RetentionDay = int32(int64Value(zap, "retention-day", int64(admin.Zap.RetentionDay)))
admin.Zap.AccessReqBody = boolValue(zap, "access-req-body", admin.Zap.AccessReqBody)
admin.Zap.AccessRespData = boolValue(zap, "access-resp-data", admin.Zap.AccessRespData)
admin.Zap.AccessReqHeaders = boolValue(zap, "access-req-headers", admin.Zap.AccessReqHeaders)
admin.Zap.AccessLogMaxBytes = int32(int64Value(zap, "access-log-max-bytes", int64(admin.Zap.AccessLogMaxBytes)))
if list, ok := zap["file-only-modules"].([]any); ok {
admin.Zap.FileOnlyModules = admin.Zap.FileOnlyModules[:0]
for _, item := range list {
if value, ok := item.(string); ok {
admin.Zap.FileOnlyModules = append(admin.Zap.FileOnlyModules, value)
}
}
}
if data.Redis == nil {
data.Redis = &conf.Data_Redis{}
}
applyGVARedis(data.Redis, objectMap(values["redis"]))
if items, ok := values["redis-list"].([]any); ok {
current := make(map[string]*conf.Data_Redis, len(data.RedisList))
for _, item := range data.RedisList {
if item != nil {
current[item.Name] = item
}
}
data.RedisList = make([]*conf.Data_Redis, 0, len(items))
for _, item := range items {
values := objectMap(item)
target := current[stringValue(values, "name")]
if target == nil {
target = &conf.Data_Redis{}
}
applyGVARedis(target, values)
data.RedisList = append(data.RedisList, target)
}
}
if mongo := objectMap(values["mongo"]); mongo != nil {
if data.Mongo == nil {
data.Mongo = &conf.Data_Mongo{}
}
data.Mongo.Coll, data.Mongo.Options, data.Mongo.Database = stringValue(mongo, "coll"), stringValue(mongo, "options"), stringValue(mongo, "database")
data.Mongo.Username, data.Mongo.AuthSource = stringValue(mongo, "username"), stringValue(mongo, "auth-source")
if password := stringValue(mongo, "password"); password != "" && password != "******" {
data.Mongo.Password = password
}
data.Mongo.MinPoolSize = uint64(int64Value(mongo, "min-pool-size", int64(data.Mongo.MinPoolSize)))
data.Mongo.MaxPoolSize = uint64(int64Value(mongo, "max-pool-size", int64(data.Mongo.MaxPoolSize)))
data.Mongo.SocketTimeoutMs = int64Value(mongo, "socket-timeout-ms", data.Mongo.SocketTimeoutMs)
data.Mongo.ConnectTimeoutMs = int64Value(mongo, "connect-timeout-ms", data.Mongo.ConnectTimeoutMs)
data.Mongo.IsZap = boolValue(mongo, "is-zap", data.Mongo.IsZap)
if hosts, ok := mongo["hosts"].([]any); ok {
data.Mongo.Hosts = make([]*conf.Data_MongoHost, 0, len(hosts))
for _, item := range hosts {
host := objectMap(item)
data.Mongo.Hosts = append(data.Mongo.Hosts, &conf.Data_MongoHost{Host: stringValue(host, "host"), Port: stringValue(host, "port")})
}
}
}
if items, ok := values["db-list"].([]any); ok {
current := make(map[string]*conf.Data_Database, len(data.DatabaseList))
for _, item := range data.DatabaseList {
if item != nil {
current[item.AliasName] = item
}
}
data.DatabaseList = make([]*conf.Data_Database, 0, len(items))
for _, item := range items {
row := objectMap(item)
alias := stringValue(row, "alias-name")
target := current[alias]
if target == nil {
target = &conf.Data_Database{}
}
applyGVADatabase(target, row, stringValue(row, "type"))
target.AliasName, target.Disable = alias, boolValue(row, "disable", target.Disable)
data.DatabaseList = append(data.DatabaseList, target)
}
}
if admin.Email == nil {
admin.Email = &conf.AdminBackend_Email{}
}
email := objectMap(values["email"])
admin.Email.To, admin.Email.From, admin.Email.Host = stringValue(email, "to"), stringValue(email, "from"), stringValue(email, "host")
admin.Email.Nickname = stringValue(email, "nickname")
admin.Email.Port = int32(int64Value(email, "port", int64(admin.Email.Port)))
admin.Email.IsSsl, admin.Email.IsLoginAuth = boolValue(email, "is-ssl", admin.Email.IsSsl), boolValue(email, "is-loginauth", admin.Email.IsLoginAuth)
if secret := stringValue(email, "secret"); secret != "" && secret != "******" {
admin.Email.Secret = secret
}
if admin.Local == nil {
admin.Local = &conf.AdminBackend_Local{}
}
local := objectMap(values["local"])
admin.Local.PathPrefix, admin.Local.StorePath = stringValue(local, "path"), stringValue(local, "store-path")
if admin.Media == nil {
admin.Media = &conf.AdminBackend_Media{}
}
media := objectMap(values["media"])
admin.Media.SessionTtl = int32(int64Value(media, "session-ttl", int64(admin.Media.SessionTtl)))
admin.Media.MaxFileSize = int64Value(media, "max-file-size", admin.Media.MaxFileSize)
admin.Media.ChunkDir = stringValue(media, "chunk-dir")
if admin.Storage.Qiniu == nil {
admin.Storage.Qiniu = &conf.AdminBackend_Qiniu{}
}
qiniu := objectMap(values["qiniu"])
admin.Storage.Qiniu.Zone, admin.Storage.Qiniu.Bucket, admin.Storage.Qiniu.BaseUrl = stringValue(qiniu, "zone"), stringValue(qiniu, "bucket"), stringValue(qiniu, "img-path")
admin.Storage.Qiniu.AccessKey = stringValue(qiniu, "access-key")
if secret := stringValue(qiniu, "secret-key"); secret != "" && secret != "******" {
admin.Storage.Qiniu.SecretKey = secret
}
admin.Storage.Qiniu.UseHttps, admin.Storage.Qiniu.UseCdnDomains = boolValue(qiniu, "use-https", admin.Storage.Qiniu.UseHttps), boolValue(qiniu, "use-cdn-domains", admin.Storage.Qiniu.UseCdnDomains)
stores := []struct {
key string
target **conf.AdminBackend_ObjectStore
}{{"aliyun-oss", &admin.Storage.AliyunOss}, {"hua-wei-obs", &admin.Storage.HuaweiObs}, {"tencent-cos", &admin.Storage.TencentCos}, {"aws-s3", &admin.Storage.AwsS3}, {"cloudflare-r2", &admin.Storage.CloudflareR2}, {"minio", &admin.Storage.Minio}}
for _, store := range stores {
if *store.target == nil {
*store.target = &conf.AdminBackend_ObjectStore{}
}
applyGVAObjectStore(*store.target, objectMap(values[store.key]))
}
if disks, ok := values["disk-list"].([]any); ok {
admin.DiskList = make([]*conf.AdminBackend_Disk, 0, len(disks))
for _, item := range disks {
disk := objectMap(item)
admin.DiskList = append(admin.DiskList, &conf.AdminBackend_Disk{MountPoint: stringValue(disk, "mount-point")})
}
}
if cors := objectMap(values["cors"]); cors != nil {
if admin.Cors == nil {
admin.Cors = &conf.AdminBackend_CORS{}
}
admin.Cors.Mode = stringValue(cors, "mode")
if rules, ok := cors["whitelist"].([]any); ok {
admin.Cors.Whitelist = make([]*conf.AdminBackend_CORSRule, 0, len(rules))
for _, item := range rules {
rule := objectMap(item)
admin.Cors.Whitelist = append(admin.Cors.Whitelist, &conf.AdminBackend_CORSRule{AllowOrigin: stringValue(rule, "allow-origin"), AllowMethods: stringValue(rule, "allow-methods"), AllowHeaders: stringValue(rule, "allow-headers"), ExposeHeaders: stringValue(rule, "expose-headers"), AllowCredentials: boolValue(rule, "allow-credentials", false)})
}
}
}
if app := objectMap(values["app"]); app != nil {
if admin.App == nil {
admin.App = &conf.AdminBackend_App{}
}
admin.App.Node, admin.App.AppId, admin.App.Env = stringValue(app, "node"), stringValue(app, "app-id"), stringValue(app, "env")
}
return nil
}
+344 -97
View File
@@ -4,8 +4,11 @@ import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
pathpkg "path"
"path/filepath"
"sort"
"strings"
@@ -14,86 +17,320 @@ import (
"kra/internal/biz"
)
func (r *logFileRepo) logRoot() (string, error) {
const (
defaultLogChunkLines = 500
maxLogChunkBytes = 2 * 1024 * 1024
logReadBlockSize = 64 * 1024
)
func (r *logFileRepo) configuredLogRoot() (root string, exists bool, err error) {
admin := r.data.runtime.Admin()
if admin == nil || admin.Zap == nil || strings.TrimSpace(admin.Zap.Director) == "" {
return "", biz.ErrLogRootUnavailable
return "", false, biz.ErrLogRootUnavailable
}
root, err := filepath.Abs(admin.Zap.Director)
root, err = filepath.Abs(admin.Zap.Director)
if err != nil {
return "", errors.Join(biz.ErrLogRootUnavailable, err)
return "", false, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
info, err := os.Stat(root)
if errors.Is(err, fs.ErrNotExist) {
return root, nil
return root, false, nil
}
if err != nil {
return "", errors.Join(biz.ErrLogRootUnavailable, err)
return "", false, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
if !info.IsDir() {
return "", biz.ErrLogRootUnavailable
return "", false, biz.ErrLogRootUnavailable
}
return root, nil
return root, true, nil
}
func (r *logFileRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate, error) {
if parsed, err := time.Parse("2006-01", month); err != nil || parsed.Format("2006-01") != month {
return nil, biz.ErrInvalidLogMonth
func (r *logFileRepo) openConfiguredLogRoot() (root *os.Root, exists bool, err error) {
rootPath, exists, err := r.configuredLogRoot()
if err != nil || !exists {
return nil, exists, err
}
root, err := r.logRoot()
root, err = os.OpenRoot(rootPath)
if err != nil {
return nil, false, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
return root, true, nil
}
func (r *logFileRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate, error) {
if err := validateLogMonth(month); err != nil {
return nil, err
}
logRoot, exists, err := r.openConfiguredLogRoot()
if err != nil {
return nil, err
}
entries, err := os.ReadDir(root)
if errors.Is(err, fs.ErrNotExist) {
if !exists {
return []biz.LogDate{}, nil
}
defer logRoot.Close()
entries, err := fs.ReadDir(logRoot.FS(), ".")
if err != nil {
return nil, err
return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
out := []biz.LogDate{}
result := make([]biz.LogDate, 0)
for _, entry := range entries {
if ctx.Err() != nil {
return nil, ctx.Err()
if err = ctx.Err(); err != nil {
return nil, err
}
if entry.Type()&os.ModeSymlink != 0 || !entry.IsDir() || !strings.HasPrefix(entry.Name(), month+"-") {
continue
}
if parsed, parseErr := time.Parse("2006-01-02", entry.Name()); parseErr != nil || parsed.Format("2006-01-02") != entry.Name() {
if validateLogDate(entry.Name()) != nil {
continue
}
count := 0
_ = filepath.WalkDir(filepath.Join(root, entry.Name()), func(_ string, item fs.DirEntry, walkErr error) error {
if walkErr == nil && !item.IsDir() && strings.EqualFold(filepath.Ext(item.Name()), ".log") {
count++
}
return nil
})
count, countErr := countLogFiles(ctx, logRoot, entry.Name())
if countErr != nil {
return nil, countErr
}
if count > 0 {
out = append(out, biz.LogDate{Date: entry.Name(), FileCount: count})
result = append(result, biz.LogDate{Date: entry.Name(), FileCount: count})
}
}
sort.Slice(out, func(i, j int) bool { return out[i].Date < out[j].Date })
return out, nil
sort.Slice(result, func(i, j int) bool { return result[i].Date < result[j].Date })
return result, nil
}
func (r *logFileRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile, error) {
if parsed, err := time.Parse("2006-01-02", date); err != nil || parsed.Format("2006-01-02") != date {
return nil, biz.ErrInvalidLogDate
if err := validateLogDate(date); err != nil {
return nil, err
}
root, err := r.logRoot()
logRoot, exists, err := r.openConfiguredLogRoot()
if err != nil {
return nil, err
}
dateRoot := filepath.Join(root, date)
out := []biz.LogFile{}
err = filepath.WalkDir(dateRoot, func(path string, entry fs.DirEntry, walkErr error) error {
if !exists {
return []biz.LogFile{}, nil
}
defer logRoot.Close()
info, err := logRoot.Lstat(date)
if errors.Is(err, fs.ErrNotExist) {
return []biz.LogFile{}, nil
}
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return []biz.LogFile{}, nil
}
dateRoot, err := logRoot.OpenRoot(date)
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
defer dateRoot.Close()
result := make([]biz.LogFile, 0)
err = fs.WalkDir(dateRoot.FS(), ".", func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
if errors.Is(walkErr, fs.ErrNotExist) {
return nil
}
return walkErr
}
if ctx.Err() != nil {
return ctx.Err()
if err := ctx.Err(); err != nil {
return err
}
if path == "." {
return nil
}
if entry.Type()&os.ModeSymlink != 0 {
if entry.IsDir() {
return filepath.SkipDir
}
return nil
}
if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), ".log") {
return nil
}
fileInfo, infoErr := entry.Info()
if infoErr != nil {
return infoErr
}
if fileInfo.Mode().IsRegular() {
result = append(result, biz.LogFile{Path: path, Name: entry.Name(), Size: fileInfo.Size(), ModifiedAt: fileInfo.ModTime()})
}
return nil
})
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
sort.Slice(result, func(i, j int) bool { return result[i].Path < result[j].Path })
return result, nil
}
func (r *logFileRepo) LogContent(ctx context.Context, date, apiPath string, cursor *int64) (*biz.LogContent, error) {
result := &biz.LogContent{Date: date, Path: apiPath}
if err := ctx.Err(); err != nil {
return nil, err
}
if cursor != nil && *cursor < 0 {
return nil, biz.ErrInvalidLogPath
}
file, info, err := r.openValidatedLogFile(date, apiPath)
if err != nil {
return nil, err
}
defer file.Close()
end := info.Size()
if cursor != nil && *cursor <= end {
end = *cursor
}
start, limitedByBytes, err := findLogChunkStart(file, end)
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
}
data, err := readLogRange(file, start, end)
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
}
if int64(len(data)) < end-start {
currentInfo, statErr := file.Stat()
if statErr != nil || currentInfo.Size() < start {
if statErr != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, statErr)
}
return nil, biz.ErrLogFileUnreadable
}
info = currentInfo
}
result.Content = string(data)
result.LineCount = countLogicalLines(data)
result.NextCursor = start
result.HasMore = start > 0
result.LimitedByBytes = limitedByBytes
result.Size = info.Size()
result.ModifiedAt = info.ModTime()
return result, nil
}
func (r *logFileRepo) openValidatedLogFile(date, apiPath string) (file *os.File, info os.FileInfo, err error) {
if err = validateLogDate(date); err != nil {
return nil, nil, err
}
segments, err := validateLogAPIPath(apiPath)
if err != nil {
return nil, nil, err
}
logRoot, exists, err := r.openConfiguredLogRoot()
if err != nil {
return nil, nil, err
}
if !exists {
return nil, nil, biz.ErrLogFileNotFound
}
defer logRoot.Close()
dateInfo, err := logRoot.Lstat(date)
if errors.Is(err, fs.ErrNotExist) {
return nil, nil, biz.ErrLogFileNotFound
}
if err != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
}
if dateInfo.Mode()&os.ModeSymlink != 0 || !dateInfo.IsDir() {
return nil, nil, biz.ErrInvalidLogPath
}
dateRoot, err := logRoot.OpenRoot(date)
if err != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
}
defer dateRoot.Close()
relativePath := ""
var validatedInfo os.FileInfo
for index, segment := range segments {
relativePath = filepath.Join(relativePath, segment)
validatedInfo, err = dateRoot.Lstat(relativePath)
if errors.Is(err, fs.ErrNotExist) {
return nil, nil, biz.ErrLogFileNotFound
}
if err != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
}
if validatedInfo.Mode()&os.ModeSymlink != 0 {
return nil, nil, biz.ErrInvalidLogPath
}
if index < len(segments)-1 && !validatedInfo.IsDir() {
return nil, nil, biz.ErrInvalidLogPath
}
}
if validatedInfo == nil || !validatedInfo.Mode().IsRegular() {
return nil, nil, biz.ErrInvalidLogPath
}
file, err = dateRoot.Open(relativePath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, nil, biz.ErrLogFileNotFound
}
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
}
openedInfo, statErr := file.Stat()
if statErr != nil || !openedInfo.Mode().IsRegular() || !os.SameFile(validatedInfo, openedInfo) {
file.Close()
if statErr != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, statErr)
}
return nil, nil, biz.ErrInvalidLogPath
}
return file, openedInfo, nil
}
func validateLogMonth(month string) error {
parsed, err := time.Parse("2006-01", month)
if err != nil || parsed.Format("2006-01") != month {
return biz.ErrInvalidLogMonth
}
return nil
}
func validateLogDate(date string) error {
parsed, err := time.Parse("2006-01-02", date)
if err != nil || parsed.Format("2006-01-02") != date {
return biz.ErrInvalidLogDate
}
return nil
}
func validateLogAPIPath(apiPath string) ([]string, error) {
if apiPath == "" || strings.Contains(apiPath, "\\") || strings.Contains(apiPath, ":") || pathpkg.IsAbs(apiPath) {
return nil, biz.ErrInvalidLogPath
}
segments := strings.Split(apiPath, "/")
for _, segment := range segments {
if segment == "" || segment == "." || segment == ".." {
return nil, biz.ErrInvalidLogPath
}
}
if !strings.EqualFold(pathpkg.Ext(apiPath), ".log") {
return nil, biz.ErrInvalidLogPath
}
return segments, nil
}
func countLogFiles(ctx context.Context, logRoot *os.Root, date string) (count int, err error) {
dateInfo, err := logRoot.Lstat(date)
if err != nil || dateInfo.Mode()&os.ModeSymlink != 0 || !dateInfo.IsDir() {
return 0, err
}
dateRoot, err := logRoot.OpenRoot(date)
if err != nil {
return 0, err
}
defer dateRoot.Close()
err = fs.WalkDir(dateRoot.FS(), ".", func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if err := ctx.Err(); err != nil {
return err
}
if path == "." {
return nil
}
if entry.Type()&os.ModeSymlink != 0 {
if entry.IsDir() {
@@ -108,70 +345,80 @@ func (r *logFileRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile,
if infoErr != nil {
return infoErr
}
relative, _ := filepath.Rel(dateRoot, path)
out = append(out, biz.LogFile{Path: filepath.ToSlash(relative), Name: entry.Name(), Size: info.Size(), ModifiedAt: info.ModTime()})
if info.Mode().IsRegular() {
count++
}
return nil
})
sort.Slice(out, func(i, j int) bool { return out[i].Path < out[j].Path })
return out, err
}
func (r *logFileRepo) LogContent(ctx context.Context, date, path string, cursor *int64) (*biz.LogContent, error) {
if parsed, err := time.Parse("2006-01-02", date); err != nil || parsed.Format("2006-01-02") != date {
return nil, biz.ErrInvalidLogDate
}
root, err := r.logRoot()
if err != nil {
return 0, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
return count, nil
}
func readLogRange(file *os.File, start, end int64) ([]byte, error) {
if end <= start {
return []byte{}, nil
}
data := make([]byte, int(end-start))
n, err := file.ReadAt(data, start)
if err != nil && !errors.Is(err, io.EOF) {
return nil, err
}
dateRoot := filepath.Join(root, date)
target := filepath.Clean(filepath.Join(dateRoot, filepath.FromSlash(path)))
if target == dateRoot || !strings.HasPrefix(target, dateRoot+string(os.PathSeparator)) || !strings.EqualFold(filepath.Ext(target), ".log") {
return nil, biz.ErrInvalidLogPath
}
info, err := os.Lstat(target)
if errors.Is(err, fs.ErrNotExist) {
return nil, biz.ErrLogFileNotFound
}
if err != nil {
return nil, errors.Join(biz.ErrLogFileUnreadable, err)
return data[:n], nil
}
func findLogChunkStart(file *os.File, end int64) (start int64, limitedByBytes bool, err error) {
if end <= 0 {
return 0, false, nil
}
lastByte := []byte{0}
if _, err = file.ReadAt(lastByte, end-1); err != nil {
return 0, false, err
}
targetNewlines := defaultLogChunkLines
if lastByte[0] == '\n' {
targetNewlines++
}
position, scanned, newlines := end, int64(0), 0
for position > 0 && scanned < maxLogChunkBytes {
readSize := int64(logReadBlockSize)
if readSize > position {
readSize = position
}
if remaining := int64(maxLogChunkBytes) - scanned; readSize > remaining {
readSize = remaining
}
blockStart := position - readSize
block := make([]byte, int(readSize))
n, readErr := file.ReadAt(block, blockStart)
if readErr != nil && !errors.Is(readErr, io.EOF) {
return 0, false, readErr
}
for index := n - 1; index >= 0; index-- {
if block[index] == '\n' {
newlines++
if newlines == targetNewlines {
return blockStart + int64(index) + 1, false, nil
}
}
}
scanned += int64(n)
position = blockStart
}
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
return nil, biz.ErrInvalidLogPath
if position > 0 {
return end - int64(maxLogChunkBytes), true, nil
}
end := info.Size()
if cursor != nil && *cursor >= 0 && *cursor < end {
end = *cursor
}
start := end - int64(2*1024*1024)
limited := start > 0
if start < 0 {
start = 0
}
file, err := os.Open(target)
if err != nil {
return nil, errors.Join(biz.ErrLogFileUnreadable, err)
}
defer file.Close()
data := make([]byte, end-start)
n, err := file.ReadAt(data, start)
if err != nil && n == 0 {
return nil, errors.Join(biz.ErrLogFileUnreadable, err)
}
data = data[:n]
lines := bytes.Split(data, []byte("\n"))
if len(lines) > 501 {
drop := len(lines) - 501
offset := 0
for _, line := range lines[:drop] {
offset += len(line) + 1
}
start += int64(offset)
data = data[offset:]
limited = true
return 0, false, nil
}
func countLogicalLines(data []byte) int {
if len(data) == 0 {
return 0
}
lineCount := bytes.Count(data, []byte("\n"))
if len(data) > 0 && data[len(data)-1] != '\n' {
lineCount++
count := bytes.Count(data, []byte{'\n'})
if data[len(data)-1] != '\n' {
count++
}
return &biz.LogContent{Date: date, Path: path, Content: string(data), LineCount: lineCount, NextCursor: start, HasMore: start > 0, LimitedByBytes: limited, Size: info.Size(), ModifiedAt: info.ModTime()}, nil
return count
}
+2 -2
View File
@@ -68,7 +68,7 @@ func (s *runtimeSettings) MediaSettings() biz.MediaSettings {
if config == nil || config.Media == nil {
return biz.MediaSettings{}
}
return biz.MediaSettings{SessionTTL: int(config.Media.SessionTtl), MaxFileSize: config.Media.MaxFileSize}
return biz.MediaSettings{SessionTTL: int(config.Media.SessionTtl), MaxFileSize: config.Media.MaxFileSize, ChunkDir: config.Media.ChunkDir}
}
func (s *runtimeSettings) UseMultipoint() bool {
@@ -110,5 +110,5 @@ func (i *tokenIssuer) ParseToken(token string) (*biz.AuthClaims, error) {
return nil, biz.ErrTokenInvalid
}
}
return &biz.AuthClaims{UUID: claims.UUID, ID: claims.ID, Username: claims.Username, NickName: claims.NickName, AuthorityID: claims.AuthorityID, BufferTime: time.Duration(claims.BufferTime) * time.Second, MustChangePwd: claims.MustChangePwd, Issuer: claims.Issuer, ExpiresAt: claims.ExpiresAt.Time}, nil
return &biz.AuthClaims{UUID: claims.UUID, ID: claims.ID, Username: claims.Username, NickName: claims.NickName, AuthorityID: claims.AuthorityID, UserType: claims.UserType, BufferTime: time.Duration(claims.BufferTime) * time.Second, MustChangePwd: claims.MustChangePwd, Issuer: claims.Issuer, ExpiresAt: claims.ExpiresAt.Time}, nil
}
+3 -1
View File
@@ -95,7 +95,9 @@ func (s *aliyunStorage) List(_ context.Context, prefix, cursor string, limit int
}
items := make([]*biz.StoredFile, 0, len(result.Objects))
for _, object := range result.Objects {
items = append(items, s.file(object.Key, object.Size))
item := s.file(object.Key, object.Size)
item.LastModified = object.LastModified
items = append(items, item)
}
next := ""
if result.IsTruncated {
+5 -1
View File
@@ -141,7 +141,11 @@ func (s *awsStorage) List(ctx context.Context, prefix, cursor string, limit int)
if object.Size != nil {
size = *object.Size
}
items = append(items, s.file(*object.Key, size))
item := s.file(*object.Key, size)
if object.LastModified != nil {
item.LastModified = *object.LastModified
}
items = append(items, item)
}
next := ""
if result.NextContinuationToken != nil {
+3 -1
View File
@@ -94,7 +94,9 @@ func (s *huaweiStorage) List(_ context.Context, prefix, cursor string, limit int
}
items := make([]*biz.StoredFile, 0, len(result.Contents))
for _, object := range result.Contents {
items = append(items, s.file(object.Key, object.Size))
item := s.file(object.Key, object.Size)
item.LastModified = object.LastModified
items = append(items, item)
}
next := result.NextMarker
if result.IsTruncated && next == "" && len(result.Contents) > 0 {
+2 -1
View File
@@ -6,6 +6,7 @@ import (
"encoding/hex"
"fmt"
"io"
"mime"
"os"
"path/filepath"
"sort"
@@ -200,7 +201,7 @@ func (s *fileStorage) List(ctx context.Context, prefix, cursor string, limit int
if infoErr != nil {
return infoErr
}
items = append(items, &biz.StoredFile{Name: entry.Name(), Path: relative, URL: s.urlPrefix + "/" + relative, Size: info.Size()})
items = append(items, &biz.StoredFile{Name: entry.Name(), Path: relative, URL: s.urlPrefix + "/" + relative, Size: info.Size(), LastModified: info.ModTime(), ContentType: mime.TypeByExtension(filepath.Ext(entry.Name()))})
return nil
})
if err != nil {
+4 -1
View File
@@ -120,7 +120,10 @@ func (s *qiniuStorage) List(ctx context.Context, prefix, cursor string, limit in
}
out := make([]*biz.StoredFile, 0, len(entries))
for _, entry := range entries {
out = append(out, s.file(entry.Key, entry.Fsize))
item := s.file(entry.Key, entry.Fsize)
item.LastModified = time.Unix(0, entry.PutTime*100)
item.ContentType = entry.MimeType
out = append(out, item)
}
return out, marker, more, nil
}
+4 -1
View File
@@ -118,7 +118,10 @@ func (s *s3Storage) List(ctx context.Context, prefix, cursor string, limit int)
if item.Err != nil {
return nil, "", false, item.Err
}
out = append(out, s.file(item.Key, item.Size))
file := s.file(item.Key, item.Size)
file.LastModified = item.LastModified
file.ContentType = item.ContentType
out = append(out, file)
if len(out) > limit {
break
}
+9 -1
View File
@@ -8,6 +8,7 @@ import (
"net/url"
"path"
"strings"
"time"
cos "github.com/tencentyun/cos-go-sdk-v5"
"kra/internal/biz"
@@ -107,7 +108,14 @@ func (s *tencentStorage) List(ctx context.Context, prefix, cursor string, limit
}
items := make([]*biz.StoredFile, 0, len(result.Contents))
for _, object := range result.Contents {
items = append(items, s.file(object.Key, object.Size))
item := s.file(object.Key, object.Size)
for _, layout := range []string{time.RFC3339, "2006-01-02T15:04:05.000Z"} {
if parsed, parseErr := time.Parse(layout, object.LastModified); parseErr == nil {
item.LastModified = parsed
break
}
}
items = append(items, item)
}
next := result.NextMarker
if result.IsTruncated && next == "" && len(result.Contents) > 0 {
+4 -1
View File
@@ -429,6 +429,9 @@ func (r *userRepo) UpdateUserWithAuthorities(ctx context.Context, user *biz.User
}
func (r *userRepo) DeleteUser(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("id = ?", id).Delete(&userPO{}).Error; err != nil {
return err
}
if err := tx.Where("sys_user_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil {
return err
}
@@ -438,7 +441,7 @@ func (r *userRepo) DeleteUser(ctx context.Context, id uint) error {
if err := tx.Where("sys_user_id = ?", id).Delete(&userPositionPO{}).Error; err != nil {
return err
}
return tx.Delete(&userPO{}, id).Error
return nil
})
}
func (r *userRepo) UpdatePassword(ctx context.Context, id uint, password string, clearMustChange bool) error {