修改存储结构

This commit is contained in:
keac
2026-01-23 13:05:02 +08:00
parent 080e45e4a6
commit af0a6bc3c1
6 changed files with 343 additions and 41 deletions

2
go.mod
View File

@@ -12,9 +12,11 @@ require (
github.com/schollz/progressbar/v3 v3.19.0
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
go.etcd.io/bbolt v1.3.7
golang.org/x/crypto v0.45.0
golang.org/x/term v0.37.0
golang.org/x/text v0.31.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
)

4
go.sum
View File

@@ -239,6 +239,8 @@ github.com/zclconf/go-cty v1.16.4 h1:QGXaag7/7dCzb+odlGrgr+YmYZFaOCMW6DEpS+UD1eE
github.com/zclconf/go-cty v1.16.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -412,6 +414,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

View File

@@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"red-cloud/mod/gologger"
"red-cloud/pkg/store"
"red-cloud/utils"
"strings"
"text/tabwriter"
@@ -24,6 +25,7 @@ const (
StateError CaseState = "error"
StateCreated CaseState = "created"
StatePending CaseState = "pending"
StateUnknown CaseState = "unknown"
)
func RandomName(s string) string {
@@ -76,7 +78,7 @@ func CaseScene(t string, m map[string]string) ([]string, error) {
fmt.Sprintf("node_count=%d", Node),
fmt.Sprintf("domain=%s", Domain),
)
case "proxy","aws-proxy", "aliyun-proxy", "asm":
case "proxy", "aws-proxy", "aliyun-proxy", "asm":
par = RVar(fmt.Sprintf("node_count=%d", Node))
case "dnslog", "xraydnslog", "interactsh":
if Domain == "360.com" {
@@ -287,12 +289,12 @@ func (c *Case) TfOutput() (map[string]tfexec.OutputMeta, error) {
// bindHandlers 绑定项目方法
func (c *Case) bindHandlers(p *RedcProject) {
// 随时删除自己
c.removeHandle = func() error {
return p.HandleCase(c)
}
c.saveHandler = func() error {
return p.SaveProject()
// 保存单个 Case不再保存整个 Project
return store.SaveCase(p.ProjectName, c)
}
c.removeHandle = func() error {
return store.DeleteCase(p.ProjectName, c.Id)
}
}

View File

@@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"red-cloud/mod/gologger"
"red-cloud/pkg/store"
"strings"
"text/tabwriter"
"time"
@@ -36,6 +37,7 @@ type Case struct {
StateTime string `json:"state_time"`
Parameter []string `json:"parameter"`
State CaseState `json:"state"`
Output string
output map[string]tfexec.OutputMeta
saveHandler func() error
removeHandle func() error
@@ -66,7 +68,6 @@ func NewProjectConfig(name string, user string) (*RedcProject, error) {
if err := os.MkdirAll(path, 0755); err != nil {
return nil, fmt.Errorf("创建项目目录失败: %w", err)
}
gologger.Info().Msgf("项目目录「%s」创建成功", name)
// 创建项目状态文件
currentTime := time.Now().Format("2006-01-02 15:04:05")
project := &RedcProject{
@@ -76,9 +77,9 @@ func NewProjectConfig(name string, user string) (*RedcProject, error) {
User: user,
}
if err := project.SaveProject(); err != nil {
// 如果保存失败,应该清理目录吗?视业务逻辑而定,这里暂时只返回错误
return nil, fmt.Errorf("保存项目状态文件失败: %w", err)
// 保存到 DB
if err := store.SaveProjectMeta(project); err != nil {
return nil, fmt.Errorf("保存数据库失败: %v", err)
}
gologger.Info().Msgf("项目状态文件「%s」创建成功", ProjectFile)
return project, nil
@@ -87,23 +88,14 @@ func NewProjectConfig(name string, user string) (*RedcProject, error) {
func ProjectParse(name string, user string) (*RedcProject, error) {
// 尝试直接读取项目
if p, err := ProjectByName(name); err == nil {
if p, err := store.GetProjectMeta(name); err == nil {
// 项目鉴权
if p.User != user && user != "system" {
return nil, fmt.Errorf("当前用户「%s」无权限访问项目「%s」", user, name)
}
// 读取成功,直接返回
return p, nil
}
path := filepath.Join(ProjectPath, name)
// 检查目录是否存在,或者直接尝试创建
if _, statErr := os.Stat(path); os.IsNotExist(statErr) {
gologger.Info().Msgf("项目不存在,正在创建新项目: %s", name)
return NewProjectConfig(name, user)
} else if statErr != nil {
// 目录存在但有其他错误(如权限不足)
return nil, statErr
}
gologger.Info().Msgf("项目不存在,正在创建新项目: %s", name)
return NewProjectConfig(name, user)
}
@@ -128,6 +120,10 @@ func ProjectByName(name string) (*RedcProject, error) {
// 逻辑参考 Docker: 优先精确匹配,其次 ID 前缀匹配。如果 ID 前缀匹配到多个,则报错歧义。
func (p *RedcProject) GetCase(identifier string) (*Case, error) {
var candidates []*Case
if len(p.Case) == 0 {
cases, _ := store.ListCases(p.ProjectName)
p.Case = cases
}
// 遍历所有 Case
for i := range p.Case {
@@ -170,31 +166,45 @@ func (p *RedcProject) GetCase(identifier string) (*Case, error) {
// HandleCase 删除指定uid的case
func (p *RedcProject) HandleCase(c *Case) error {
uid := c.Id
found := false
for i, caseInfo := range p.Case {
if caseInfo.Id == uid {
// 执行删除逻辑:将 i 之后的所有元素前移
p.Case = append(p.Case[:i], p.Case[i+1:]...)
found = true
break // 找到并删除后立即退出循环
}
// 调用 store 直接删除
if err := store.DeleteCase(p.ProjectName, c.Id); err != nil {
return err
}
if !found {
return fmt.Errorf("未找到 UID 为 %s 的 case无需删除", uid)
}
// 3. 将修改后的 project 写回文件
err := p.SaveProject()
if err != nil {
return fmt.Errorf("更新项目文件失败: %v", err)
}
return nil
//uid := c.Id
//found := false
//for i, caseInfo := range p.Case {
// if caseInfo.Id == uid {
// // 执行删除逻辑:将 i 之后的所有元素前移
// p.Case = append(p.Case[:i], p.Case[i+1:]...)
// found = true
// break // 找到并删除后立即退出循环
// }
//}
//
//if !found {
// return fmt.Errorf("未找到 UID 为 %s 的 case无需删除", uid)
//}
//
//// 3. 将修改后的 project 写回文件
//err := p.SaveProject()
//if err != nil {
// return fmt.Errorf("更新项目文件失败: %v", err)
//}
//
//return nil
}
func (p *RedcProject) AddCase(c *Case) error {
// 绑定 handler
c.bindHandlers(p)
// 保存到 DB
if err := c.saveHandler(); err != nil {
return err
}
// 更新内存
p.Case = append(p.Case, c)
return nil
}

246
pkg/store/store.go Normal file
View File

@@ -0,0 +1,246 @@
package store
import (
"encoding/json"
"errors"
"fmt"
"time"
"red-cloud/mod"
"red-cloud/proto"
bolt "go.etcd.io/bbolt"
"google.golang.org/protobuf/proto"
)
const (
DBPath = "redc.db"
BucketProjectMeta = "Project_Meta"
)
// getCaseBucketName 获取存放特定项目 Case 的桶名
func getCaseBucketName(projectName string) []byte {
return []byte(fmt.Sprintf("Cases_%s", projectName))
}
// execute 统一的带超时事务执行器
func execute(fn func(tx *bolt.Tx) error) error {
// 设置 5 秒超时,解决多进程并发冲突
opts := &bolt.Options{Timeout: 5 * time.Second}
db, err := bolt.Open(DBPath, 0600, opts)
if err != nil {
if errors.Is(err, bolt.ErrTimeout) {
return fmt.Errorf("保存配置失败!")
}
return err
}
defer db.Close()
return db.Update(fn)
}
// ==========================================
// 转换逻辑 (Mapper)
// ==========================================
// ToProto Domain(mod.Case) -> Storage(pb.Case)
func ToProto(c *mod.Case) *pb.Case {
// 处理 output map序列化存入
outputBytes, _ := json.Marshal(c.Output)
return &pb.Case{
Id: c.Id,
Name: c.Name,
Type: c.Type,
Module: c.Module,
Operator: c.Operator,
Path: c.Path,
Node: int32(c.Node),
CreateTime: c.CreateTime,
StateTime: c.StateTime,
Parameter: c.Parameter,
// 假设 mod.CaseState 底层是 string这里需要一个映射转换
// 如果 mod.CaseState 是 string这里需要手动 switch case 转 enum
// 这里简化演示假设你改造成了 int 或者做好了 map 映射
State: pb.CaseState(convertStateStringToInt(c.State)),
OutputJson: string(outputBytes),
}
}
// FromProto Storage(pb.Case) -> Domain(mod.Case)
func FromProto(p *pb.Case) *mod.Case {
c := &mod.Case{
Id: p.Id,
Name: p.Name,
Type: p.Type,
Module: p.Module,
Operator: p.Operator,
Path: p.Path,
Node: int(p.Node),
CreateTime: p.CreateTime,
StateTime: p.StateTime,
Parameter: p.Parameter,
State: convertStateEnumToString(p.State),
}
// 还原 Output
if len(p.OutputJson) > 0 {
// 注意:这里需要 mod 包暴露 Output 字段,或者你通过 SetOutput 方法赋值
json.Unmarshal([]byte(p.OutputJson), &c.Output)
}
// 绑定运行时函数 (saveHandler 等)
// 这一步通常在业务层调用更合适,或者这里不处理,由业务层 Load 完数据后统一 Bind
return c
}
// 辅助转换函数 (你需要根据你的实际 State 定义来实现)
func convertStateStringToInt(s mod.CaseState) pb.CaseState {
switch s {
case mod.StateRunning:
return pb.CaseState_RUNNING
case mod.StateStopped:
return pb.CaseState_STOPPED
default:
return pb.CaseState_UNKNOWN
}
}
func convertStateEnumToString(s pb.CaseState) mod.CaseState {
switch s {
case pb.CaseState_RUNNING:
return mod.StateRunning
case pb.CaseState_STOPPED:
return mod.StateStopped
default:
return mod.StateUnknown
}
}
// ==========================================
// 数据库操作 (对外接口)
// ==========================================
// SaveCase 保存单个 Case
func SaveCase(projectName string, c *mod.Case) error {
return execute(func(tx *bolt.Tx) error {
// 1. 确保桶存在
b, err := tx.CreateBucketIfNotExists(getCaseBucketName(projectName))
if err != nil {
return err
}
// 2. 转换
pbData := ToProto(c)
// 3. 序列化
data, err := proto.Marshal(pbData)
if err != nil {
return err
}
// 4. 存入
return b.Put([]byte(c.Id), data)
})
}
// GetCase 读取单个 Case
func GetCase(projectName string, caseId string) (*mod.Case, error) {
var p pb.Case
err := execute(func(tx *bolt.Tx) error {
b := tx.Bucket(getCaseBucketName(projectName))
if b == nil {
return fmt.Errorf("项目不存在")
}
data := b.Get([]byte(caseId))
if data == nil {
return fmt.Errorf("未找到 Case")
}
return proto.Unmarshal(data, &p)
})
if err != nil {
return nil, err
}
return FromProto(&p), nil
}
// DeleteCase 删除 Case
func DeleteCase(projectName string, caseId string) error {
return execute(func(tx *bolt.Tx) error {
b := tx.Bucket(getCaseBucketName(projectName))
if b == nil {
return nil
}
return b.Delete([]byte(caseId))
})
}
// ListCases 获取项目下所有 Case
func ListCases(projectName string) ([]*mod.Case, error) {
var cases []*mod.Case
err := execute(func(tx *bolt.Tx) error {
b := tx.Bucket(getCaseBucketName(projectName))
if b == nil {
return nil
}
return b.ForEach(func(k, v []byte) error {
var p pb.Case
if err := proto.Unmarshal(v, &p); err == nil {
cases = append(cases, FromProto(&p))
}
return nil
})
})
return cases, err
}
// SaveProjectMeta 保存项目元数据
func SaveProjectMeta(p *mod.RedcProject) error {
return execute(func(tx *bolt.Tx) error {
b, _ := tx.CreateBucketIfNotExists([]byte(BucketProjectMeta))
pbProj := &pb.Project{
ProjectName: p.ProjectName,
ProjectPath: p.ProjectPath,
CreateTime: p.CreateTime,
User: p.User,
}
data, _ := proto.Marshal(pbProj)
// 同时创建该项目的 Case 桶
tx.CreateBucketIfNotExists(getCaseBucketName(p.ProjectName))
return b.Put([]byte(p.ProjectName), data)
})
}
// GetProjectMeta 读取项目元数据 (不含 Cases)
func GetProjectMeta(name string) (*mod.RedcProject, error) {
var p pb.Project
err := execute(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(BucketProjectMeta))
if b == nil {
return fmt.Errorf("无项目数据")
}
data := b.Get([]byte(name))
if data == nil {
return fmt.Errorf("项目不存在")
}
return proto.Unmarshal(data, &p)
})
if err != nil {
return nil, err
}
return &mod.RedcProject{
ProjectName: p.ProjectName,
ProjectPath: p.ProjectPath,
CreateTime: p.CreateTime,
User: p.User,
// Case 列表需要在业务层调用 ListCases 单独填充
}, nil
}

38
proto/redc.proto Normal file
View File

@@ -0,0 +1,38 @@
syntax = "proto3";
package redc;
option go_package = "./pb";
// 对应 Go 中的 CaseState
enum CaseState {
UNKNOWN = 0;
CREATED = 1;
RUNNING = 2;
STOPPED = 3;
ERROR = 4;
}
message Case {
string id = 1;
string name = 2;
string type = 3;
string module = 4;
string operator = 5;
string path = 6;
int32 node = 7;
string create_time = 8;
string state_time = 9;
repeated string parameter = 10;
CaseState state = 11;
// Output 是 map[string]OutputMeta这里简化存储为 JSON 字符串
// 或者你可以定义一个 message OutputMeta 然后用 map<string, OutputMeta>
string output_json = 12;
}
message Project {
string project_name = 1;
string project_path = 2;
string create_time = 3;
string user = 4;
// 不存 Case 列表
}