mirror of
https://github.com/wgpsec/redc.git
synced 2026-01-24 12:43:19 +08:00
修改存储结构
This commit is contained in:
2
go.mod
2
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
||||
14
mod/case.go
14
mod/case.go
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
246
pkg/store/store.go
Normal 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
38
proto/redc.proto
Normal 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 列表
|
||||
}
|
||||
Reference in New Issue
Block a user