| @@ -0,0 +1,366 @@ | |||
| package cmdline | |||
| import ( | |||
| "context" | |||
| "database/sql" | |||
| "encoding/json" | |||
| "fmt" | |||
| "os" | |||
| "path" | |||
| "path/filepath" | |||
| "strings" | |||
| "time" | |||
| "github.com/chzyer/readline" | |||
| "github.com/spf13/cobra" | |||
| "gitlink.org.cn/cloudream/common/pkgs/logger" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/accesstoken" | |||
| clicfg "gitlink.org.cn/cloudream/jcs-pub/client/internal/config" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/db" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/downloader" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/http" | |||
| mntcfg "gitlink.org.cn/cloudream/jcs-pub/client/internal/mount/config" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/ticktock" | |||
| corrpc "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/rpc/coordinator" | |||
| hubrpc "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/rpc/hub" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/sysevent" | |||
| ) | |||
| var confs_file = path.Join("confs", "client.config.json") | |||
| var cli_certs_path = path.Join("confs", "cli_certs") | |||
| var pub_certs_path = path.Join("confs", "pub_certs") | |||
| var ca_key_file = "ca_key.pem" | |||
| var ca_cert_file = "ca_cert.pem" | |||
| var client_cert_file = "client_cert.pem" | |||
| func init() { | |||
| cmd := cobra.Command{ | |||
| Use: "init", | |||
| Short: "initialize client configuration", | |||
| Run: func(c *cobra.Command, args []string) { | |||
| init2() | |||
| }, | |||
| } | |||
| RootCmd.AddCommand(&cmd) | |||
| } | |||
| func getPath() (exePath string, dirPath string, err error) { | |||
| exePath, err = os.Executable() | |||
| if err != nil { | |||
| return "", "", fmt.Errorf("获取执行路径失败: %w", err) | |||
| } | |||
| dirPath = filepath.Dir(exePath) | |||
| return exePath, dirPath, nil | |||
| } | |||
| func init2() error { | |||
| rl, err := readline.New("> ") | |||
| if err != nil { | |||
| fmt.Printf("初始化命令行失败: %v\n", err) | |||
| return err | |||
| } | |||
| defer rl.Close() | |||
| // 1. 检查配置文件是否存在 | |||
| _, dirPath, err := getPath() | |||
| if err != nil { | |||
| return err | |||
| } | |||
| configFilePath := filepath.Join(dirPath, confs_file) | |||
| cliCertsPath := filepath.Join(dirPath, cli_certs_path) | |||
| pubCertsPath := filepath.Join(dirPath, pub_certs_path) | |||
| _, err = os.Stat(configFilePath) | |||
| if err == nil { | |||
| fmt.Println("\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m") | |||
| fmt.Println("\033[33m⚠ 配置文件已存在!重新初始化会覆盖原配置文件\033[0m") | |||
| fmt.Println("\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m") | |||
| checkLoop: | |||
| for { | |||
| // 2. 配置文件存在,询问是否覆盖 | |||
| rl.SetPrompt("\033[36m是否继续?(y/n): \033[0m") | |||
| overwrite, err := rl.Readline() | |||
| if err != nil { | |||
| return err | |||
| } | |||
| switch strings.ToLower(strings.TrimSpace(overwrite)) { | |||
| case "y", "yes": | |||
| break checkLoop | |||
| case "n", "no": | |||
| fmt.Printf("\033[32m已保留原有配置文件,退出初始化\033[0m\n") | |||
| return nil | |||
| default: | |||
| fmt.Println("\033[31m无效输入!请输入 y/n 或 yes/no \033[0m") | |||
| } | |||
| } | |||
| } | |||
| var cfg clicfg.Config | |||
| mysqlLoop: | |||
| for { | |||
| // 3. 询问数据库配置 | |||
| rl.SetPrompt("\033[36m请输入Mysql连接地址(例如127.0.0.1:3306): \033[0m") | |||
| dbAddress, err := rl.Readline() | |||
| if err != nil { | |||
| return err | |||
| } | |||
| rl.SetPrompt("\033[36m请输入Mysql用户名(例如root): \033[0m") | |||
| dbAccount, err := rl.Readline() | |||
| if err != nil { | |||
| return err | |||
| } | |||
| dbPasswordBytes, err := rl.ReadPassword("\033[36m请输入Mysql密码(例如123456): \033[0m") | |||
| if err != nil { | |||
| return err | |||
| } | |||
| dbPassword := string(dbPasswordBytes) | |||
| rl.SetPrompt("\033[36m请输入Mysql数据库名称(例如cloudream): \033[0m") | |||
| dbName, err := rl.Readline() | |||
| if err != nil { | |||
| return err | |||
| } | |||
| cfg.DB = db.Config{ | |||
| Address: dbAddress, | |||
| Account: dbAccount, | |||
| Password: dbPassword, | |||
| DatabaseName: dbName, | |||
| } | |||
| rl.SetPrompt("\033[36m是否测试数据库连接?(y/n): \033[0m") | |||
| needTest, err := rl.Readline() | |||
| if err != nil { | |||
| return err | |||
| } | |||
| switch strings.ToLower(strings.TrimSpace(needTest)) { | |||
| case "y", "yes": | |||
| // 4. 测试数据库连接 | |||
| fmt.Printf("\033[33m正在测试数据库连接...\033[0m\n") | |||
| testDB, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s", | |||
| dbAccount, | |||
| dbPassword, | |||
| dbAddress, | |||
| dbName)) | |||
| if err != nil { | |||
| fmt.Printf("\033[31m连接创建失败: %v\033[0m\n", err) | |||
| testDB.Close() | |||
| } else { | |||
| ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | |||
| defer cancel() | |||
| err = testDB.PingContext(ctx) | |||
| if err != nil { | |||
| fmt.Printf("\033[31m数据库连接测试失败: %v\033[0m\n", err) | |||
| testDB.Close() | |||
| } else { | |||
| fmt.Printf("\033[32m✓ 数据库连接测试成功!\033[0m\n\n") | |||
| testDB.Close() | |||
| break mysqlLoop | |||
| } | |||
| } | |||
| rl.SetPrompt("\033[36m连接测试失败,是否重新输入配置?(y/n): \033[0m") | |||
| retry, err := rl.Readline() | |||
| if err != nil { | |||
| return err | |||
| } | |||
| if strings.ToLower(retry) != "y" { | |||
| fmt.Printf("\033[33m警告: 数据库连接尚未验证,将使用当前配置,如需后续更改,请查看配置文件client.config.json\033[0m\n") | |||
| break mysqlLoop | |||
| } | |||
| case "n", "no": | |||
| fmt.Printf("\033[33m警告: 数据库连接未测试,将直接使用当前配置,如需后续更改,请查看配置文件client.config.json\033[0m\n") | |||
| break mysqlLoop | |||
| default: | |||
| fmt.Println("\033[31m无效输入!请输入 y/n 或 yes/no \033[0m") | |||
| } | |||
| } | |||
| // 5. 询问证书配置 | |||
| rl.SetPrompt("\033[36m请输入HTTP API监听地址(例如127.0.0.1:3306): \033[0m") | |||
| listen, err := rl.Readline() | |||
| if err != nil { | |||
| return err | |||
| } | |||
| // 6. 生成证书 | |||
| err = os.MkdirAll(cliCertsPath, 0755) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| keyFilePath := filepath.Join(cliCertsPath, ca_key_file) | |||
| certFilePath := filepath.Join(cliCertsPath, ca_cert_file) | |||
| certRoot(cliCertsPath) | |||
| certServer(certFilePath, keyFilePath, cliCertsPath) | |||
| certClient(certFilePath, keyFilePath, cliCertsPath) | |||
| cfg.HTTP = &http.ConfigJSON{ | |||
| Enabled: true, | |||
| Listen: listen, | |||
| UserSpaceID: 0, | |||
| RootCA: path.Join(cli_certs_path, "ca_cert.pem"), | |||
| ServerCert: path.Join(cli_certs_path, "server_cert.pem"), | |||
| ServerKey: path.Join(cli_certs_path, "server_key.pem"), | |||
| ClientCerts: []string{path.Join(cli_certs_path, "client_cert.pem")}, | |||
| MaxBodySize: 5242880, | |||
| } | |||
| cloudLoop: | |||
| for { | |||
| // 7. 填写云际基础设施配置 | |||
| rl.SetPrompt("\033[36m是否连接云际存储基础设施?(y/n): \033[0m") | |||
| isConnect, err := rl.Readline() | |||
| if err != nil { | |||
| return err | |||
| } | |||
| switch strings.ToLower(strings.TrimSpace(isConnect)) { | |||
| case "y", "yes": | |||
| rl.SetPrompt("\033[36m请输入Coordinator地址: \033[0m") | |||
| coorAddress, err := rl.Readline() | |||
| if err != nil { | |||
| return err | |||
| } | |||
| cfg.CoordinatorRPC = corrpc.PoolConfigJSON{ | |||
| Address: coorAddress, | |||
| RootCA: path.Join(".", pub_certs_path, ca_cert_file), | |||
| ClientCert: path.Join(".", pub_certs_path, client_cert_file), | |||
| ClientKey: path.Join(".", pub_certs_path, client_cert_file), | |||
| } | |||
| cfg.HubRPC = hubrpc.PoolConfigJSON{ | |||
| RootCA: path.Join(".", pub_certs_path, ca_cert_file), | |||
| ClientCert: path.Join(".", pub_certs_path, client_cert_file), | |||
| ClientKey: path.Join(".", pub_certs_path, client_cert_file), | |||
| } | |||
| rl.SetPrompt("\033[36m请输入账户名(Account): \033[0m") | |||
| account, err := rl.Readline() | |||
| if err != nil { | |||
| return err | |||
| } | |||
| passwordBytes, err := rl.ReadPassword("\033[36m请输入密码(Password): \033[0m") | |||
| if err != nil { | |||
| return err | |||
| } | |||
| password := string(passwordBytes) | |||
| cfg.AccessToken = &accesstoken.Config{ | |||
| Account: account, | |||
| Password: password, | |||
| } | |||
| fmt.Printf("\033[33m注意:请将JCS-pub证书文件放置于 %s 目录下\033[0m\n", pubCertsPath) | |||
| err = os.MkdirAll(pubCertsPath, 0755) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| break cloudLoop | |||
| case "n", "no": | |||
| cfg.CoordinatorRPC.Address = "127.0.0.1:5009" | |||
| cfg.AccessToken = &accesstoken.Config{} | |||
| break cloudLoop | |||
| default: | |||
| fmt.Println("\033[31m无效输入!请输入 y/n 或 yes/no \033[0m") | |||
| } | |||
| } | |||
| // 8. 生成配置文件 | |||
| err = saveConfig(&cfg, configFilePath) | |||
| if err != nil { | |||
| fmt.Printf("\033[31m保存配置文件失败: %v\033[0m\n", err) | |||
| return err | |||
| } | |||
| fmt.Printf("\033[32m配置文件已生成: %s\033[0m\n", configFilePath) | |||
| dbLoop: | |||
| for { | |||
| // 9. 询问是否生成库表结构 | |||
| rl.SetPrompt("\033[36m是否生成数据库表?(y/n): \033[0m") | |||
| isCreate, err := rl.Readline() | |||
| if err != nil { | |||
| return err | |||
| } | |||
| switch strings.ToLower(strings.TrimSpace(isCreate)) { | |||
| case "y", "yes": | |||
| // 10. 创建库表结构 | |||
| migrate(configFilePath) | |||
| break dbLoop | |||
| case "n", "no": | |||
| fmt.Println("\033[33m请自行创建数据库表,如需更改配置,请查看配置文件client.config.json \033[0m") | |||
| break dbLoop | |||
| default: | |||
| fmt.Println("\033[31m无效输入!请输入 y/n 或 yes/no \033[0m") | |||
| } | |||
| } | |||
| return nil | |||
| } | |||
| func saveConfig(cfg *clicfg.Config, configPath string) error { | |||
| cfg.Logger = logger.Config{ | |||
| Level: "info", | |||
| Output: "file", | |||
| OutputFileName: "client.log", | |||
| OutputDirectory: path.Join(".", "logs"), | |||
| } | |||
| cfg.SysEvent = sysevent.Config{ | |||
| Enabled: false, | |||
| Address: "127.0.0.1:5672", | |||
| Account: "cloudream", | |||
| Password: "123456", | |||
| VHost: "/", | |||
| Exchange: "SysEvent", | |||
| Queue: "SysEvent", | |||
| } | |||
| cfg.Connectivity.TestInterval = 300 | |||
| cfg.Downloader = downloader.Config{ | |||
| MaxStripCacheCount: 100, | |||
| ECStripPrefetchCount: 1, | |||
| } | |||
| cfg.DownloadStrategy.HighLatencyHubMs = 35 | |||
| cfg.TickTock = ticktock.Config{ | |||
| ECFileSizeThreshold: 5242880, | |||
| AccessStatHistoryWeight: 0.8, | |||
| } | |||
| cfg.Mount = &mntcfg.Config{ | |||
| Enabled: false, | |||
| AttrTimeout: time.Second * 10, | |||
| UploadPendingTime: time.Second * 30, | |||
| CacheActiveTime: time.Minute * 1, | |||
| CacheExpireTime: time.Minute * 1, | |||
| ScanDataDirInterval: time.Minute * 10, | |||
| } | |||
| configData, err := json.MarshalIndent(cfg, "", " ") | |||
| if err != nil { | |||
| return fmt.Errorf("序列化配置失败: %w", err) | |||
| } | |||
| configData = append(configData, '\n') | |||
| err = os.WriteFile(configPath, configData, 0644) | |||
| if err != nil { | |||
| return fmt.Errorf("写入配置文件失败: %w", err) | |||
| } | |||
| return nil | |||
| } | |||