You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

init.go 10 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. package cmdline
  2. import (
  3. "context"
  4. "database/sql"
  5. "encoding/json"
  6. "fmt"
  7. "os"
  8. "path"
  9. "path/filepath"
  10. "strings"
  11. "time"
  12. "github.com/chzyer/readline"
  13. "github.com/spf13/cobra"
  14. "gitlink.org.cn/cloudream/common/pkgs/logger"
  15. "gitlink.org.cn/cloudream/jcs-pub/client/internal/accesstoken"
  16. clicfg "gitlink.org.cn/cloudream/jcs-pub/client/internal/config"
  17. "gitlink.org.cn/cloudream/jcs-pub/client/internal/db"
  18. "gitlink.org.cn/cloudream/jcs-pub/client/internal/downloader"
  19. "gitlink.org.cn/cloudream/jcs-pub/client/internal/http"
  20. mntcfg "gitlink.org.cn/cloudream/jcs-pub/client/internal/mount/config"
  21. "gitlink.org.cn/cloudream/jcs-pub/client/internal/publock"
  22. "gitlink.org.cn/cloudream/jcs-pub/client/internal/ticktock"
  23. corrpc "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/rpc/coordinator"
  24. hubrpc "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/rpc/hub"
  25. "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/sysevent"
  26. )
  27. var confs_file = path.Join("confs", "client.config.json")
  28. var cli_certs_path = path.Join("confs", "cli_certs")
  29. var pub_certs_path = path.Join("confs", "pub_certs")
  30. var ca_key_file = "ca_key.pem"
  31. var ca_cert_file = "ca_cert.pem"
  32. var client_cert_file = "client_cert.pem"
  33. var client_key_file = "client_key.pem"
  34. func init() {
  35. cmd := cobra.Command{
  36. Use: "init",
  37. Short: "initialize client configuration",
  38. Run: func(c *cobra.Command, args []string) {
  39. init2()
  40. },
  41. }
  42. RootCmd.AddCommand(&cmd)
  43. }
  44. func getPath() (exePath string, dirPath string, err error) {
  45. exePath, err = os.Executable()
  46. if err != nil {
  47. return "", "", fmt.Errorf("获取执行路径失败: %w", err)
  48. }
  49. dirPath = filepath.Dir(exePath)
  50. return exePath, dirPath, nil
  51. }
  52. func init2() error {
  53. rl, err := readline.New("> ")
  54. if err != nil {
  55. fmt.Printf("初始化命令行失败: %v\n", err)
  56. return err
  57. }
  58. defer rl.Close()
  59. // 1. 检查配置文件是否存在
  60. _, dirPath, err := getPath()
  61. if err != nil {
  62. return err
  63. }
  64. configFilePath := filepath.Join(dirPath, confs_file)
  65. cliCertsPath := filepath.Join(dirPath, cli_certs_path)
  66. pubCertsPath := filepath.Join(dirPath, pub_certs_path)
  67. _, err = os.Stat(configFilePath)
  68. if err == nil {
  69. fmt.Println("\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m")
  70. fmt.Println("\033[33m⚠ 配置文件已存在!重新初始化会覆盖原配置文件\033[0m")
  71. fmt.Println("\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m")
  72. checkLoop:
  73. for {
  74. // 2. 配置文件存在,询问是否覆盖
  75. rl.SetPrompt("\033[36m是否继续?(y/n): \033[0m")
  76. overwrite, err := rl.Readline()
  77. if err != nil {
  78. return err
  79. }
  80. switch strings.ToLower(strings.TrimSpace(overwrite)) {
  81. case "y", "yes":
  82. break checkLoop
  83. case "n", "no":
  84. fmt.Printf("\033[32m已保留原有配置文件,退出初始化\033[0m\n")
  85. return nil
  86. default:
  87. fmt.Println("\033[31m无效输入!请输入 y/n 或 yes/no \033[0m")
  88. }
  89. }
  90. }
  91. var cfg clicfg.Config
  92. mysqlLoop:
  93. for {
  94. // 3. 询问数据库配置
  95. rl.SetPrompt("\033[36m请输入Mysql连接地址(例如127.0.0.1:3306): \033[0m")
  96. dbAddress, err := rl.Readline()
  97. if err != nil {
  98. return err
  99. }
  100. rl.SetPrompt("\033[36m请输入Mysql用户名(例如root): \033[0m")
  101. dbAccount, err := rl.Readline()
  102. if err != nil {
  103. return err
  104. }
  105. dbPasswordBytes, err := rl.ReadPassword("\033[36m请输入Mysql密码(例如123456): \033[0m")
  106. if err != nil {
  107. return err
  108. }
  109. dbPassword := string(dbPasswordBytes)
  110. rl.SetPrompt("\033[36m请输入Mysql数据库名称(例如cloudream): \033[0m")
  111. dbName, err := rl.Readline()
  112. if err != nil {
  113. return err
  114. }
  115. cfg.DB = db.Config{
  116. Address: dbAddress,
  117. Account: dbAccount,
  118. Password: dbPassword,
  119. DatabaseName: dbName,
  120. }
  121. rl.SetPrompt("\033[36m是否测试数据库连接?(y/n): \033[0m")
  122. needTest, err := rl.Readline()
  123. if err != nil {
  124. return err
  125. }
  126. switch strings.ToLower(strings.TrimSpace(needTest)) {
  127. case "y", "yes":
  128. // 4. 测试数据库连接
  129. fmt.Printf("\033[33m正在测试数据库连接...\033[0m\n")
  130. testDB, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s",
  131. dbAccount,
  132. dbPassword,
  133. dbAddress,
  134. dbName))
  135. if err != nil {
  136. fmt.Printf("\033[31m连接创建失败: %v\033[0m\n", err)
  137. testDB.Close()
  138. } else {
  139. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  140. defer cancel()
  141. err = testDB.PingContext(ctx)
  142. if err != nil {
  143. fmt.Printf("\033[31m数据库连接测试失败: %v\033[0m\n", err)
  144. testDB.Close()
  145. } else {
  146. fmt.Printf("\033[32m✓ 数据库连接测试成功!\033[0m\n\n")
  147. testDB.Close()
  148. break mysqlLoop
  149. }
  150. }
  151. rl.SetPrompt("\033[36m连接测试失败,是否重新输入配置?(y/n): \033[0m")
  152. retry, err := rl.Readline()
  153. if err != nil {
  154. return err
  155. }
  156. if strings.ToLower(retry) != "y" {
  157. fmt.Printf("\033[33m警告: 数据库连接尚未验证,将使用当前配置,如需后续更改,请查看配置文件client.config.json\033[0m\n")
  158. break mysqlLoop
  159. }
  160. case "n", "no":
  161. fmt.Printf("\033[33m警告: 数据库连接未测试,将直接使用当前配置,如需后续更改,请查看配置文件client.config.json\033[0m\n")
  162. break mysqlLoop
  163. default:
  164. fmt.Println("\033[31m无效输入!请输入 y/n 或 yes/no \033[0m")
  165. }
  166. }
  167. // 5. 询问证书配置
  168. rl.SetPrompt("\033[36m请输入HTTP API监听地址(例如127.0.0.1:3306): \033[0m")
  169. listen, err := rl.Readline()
  170. if err != nil {
  171. return err
  172. }
  173. // 6. 生成证书
  174. err = os.MkdirAll(cliCertsPath, 0755)
  175. if err != nil {
  176. return err
  177. }
  178. keyFilePath := filepath.Join(cliCertsPath, ca_key_file)
  179. certFilePath := filepath.Join(cliCertsPath, ca_cert_file)
  180. certRoot(cliCertsPath)
  181. certServer(certFilePath, keyFilePath, cliCertsPath)
  182. certClient(certFilePath, keyFilePath, cliCertsPath)
  183. cfg.HTTP = &http.ConfigJSON{
  184. Enabled: true,
  185. Listen: listen,
  186. UserSpaceID: 0,
  187. RootCA: path.Join(cli_certs_path, "ca_cert.pem"),
  188. ServerCert: path.Join(cli_certs_path, "server_cert.pem"),
  189. ServerKey: path.Join(cli_certs_path, "server_key.pem"),
  190. ClientCerts: []string{path.Join(cli_certs_path, "client_cert.pem")},
  191. MaxBodySize: 5242880,
  192. }
  193. cloudLoop:
  194. for {
  195. // 7. 填写云际基础设施配置
  196. rl.SetPrompt("\033[36m是否连接云际存储基础设施?(y/n): \033[0m")
  197. isConnect, err := rl.Readline()
  198. if err != nil {
  199. return err
  200. }
  201. switch strings.ToLower(strings.TrimSpace(isConnect)) {
  202. case "y", "yes":
  203. rl.SetPrompt("\033[36m请输入Coordinator地址: \033[0m")
  204. coorAddress, err := rl.Readline()
  205. if err != nil {
  206. return err
  207. }
  208. cfg.CoordinatorRPC = corrpc.PoolConfigJSON{
  209. Address: coorAddress,
  210. RootCA: path.Join(".", pub_certs_path, ca_cert_file),
  211. ClientCert: path.Join(".", pub_certs_path, client_cert_file),
  212. ClientKey: path.Join(".", pub_certs_path, client_key_file),
  213. }
  214. cfg.HubRPC = hubrpc.PoolConfigJSON{
  215. RootCA: path.Join(".", pub_certs_path, ca_cert_file),
  216. ClientCert: path.Join(".", pub_certs_path, client_cert_file),
  217. ClientKey: path.Join(".", pub_certs_path, client_key_file),
  218. }
  219. rl.SetPrompt("\033[36m请输入账户名(Account): \033[0m")
  220. account, err := rl.Readline()
  221. if err != nil {
  222. return err
  223. }
  224. passwordBytes, err := rl.ReadPassword("\033[36m请输入密码(Password): \033[0m")
  225. if err != nil {
  226. return err
  227. }
  228. password := string(passwordBytes)
  229. cfg.AccessToken = &accesstoken.Config{
  230. Account: account,
  231. Password: password,
  232. }
  233. fmt.Printf("\033[33m注意:请将JCS-pub证书文件放置于 %s 目录下\033[0m\n", pubCertsPath)
  234. err = os.MkdirAll(pubCertsPath, 0755)
  235. if err != nil {
  236. return err
  237. }
  238. break cloudLoop
  239. case "n", "no":
  240. cfg.CoordinatorRPC.Address = "127.0.0.1:5009"
  241. cfg.AccessToken = &accesstoken.Config{}
  242. break cloudLoop
  243. default:
  244. fmt.Println("\033[31m无效输入!请输入 y/n 或 yes/no \033[0m")
  245. }
  246. }
  247. // 8. 生成配置文件
  248. err = saveConfig(&cfg, configFilePath)
  249. if err != nil {
  250. fmt.Printf("\033[31m保存配置文件失败: %v\033[0m\n", err)
  251. return err
  252. }
  253. fmt.Printf("\033[32m配置文件已生成: %s\033[0m\n", configFilePath)
  254. dbLoop:
  255. for {
  256. // 9. 询问是否生成库表结构
  257. rl.SetPrompt("\033[36m是否生成数据库表?(y/n): \033[0m")
  258. isCreate, err := rl.Readline()
  259. if err != nil {
  260. return err
  261. }
  262. switch strings.ToLower(strings.TrimSpace(isCreate)) {
  263. case "y", "yes":
  264. // 10. 创建库表结构
  265. migrate(configFilePath)
  266. break dbLoop
  267. case "n", "no":
  268. fmt.Println("\033[33m请自行创建数据库表,如需更改配置,请查看配置文件client.config.json \033[0m")
  269. break dbLoop
  270. default:
  271. fmt.Println("\033[31m无效输入!请输入 y/n 或 yes/no \033[0m")
  272. }
  273. }
  274. return nil
  275. }
  276. func saveConfig(cfg *clicfg.Config, configPath string) error {
  277. cfg.Logger = logger.Config{
  278. Level: "info",
  279. Output: "file",
  280. OutputFileName: "client.log",
  281. OutputDirectory: path.Join(".", "logs"),
  282. }
  283. cfg.SysEvent = sysevent.Config{
  284. Enabled: false,
  285. Address: "127.0.0.1:5672",
  286. Account: "cloudream",
  287. Password: "123456",
  288. VHost: "/",
  289. Exchange: "SysEvent",
  290. Queue: "SysEvent",
  291. }
  292. cfg.Connectivity.TestInterval = 300
  293. cfg.Downloader = downloader.Config{
  294. MaxStripCacheCount: 100,
  295. ECStripPrefetchCount: 1,
  296. }
  297. cfg.DownloadStrategy.HighLatencyHubMs = 35
  298. cfg.TickTock = ticktock.Config{
  299. ECFileSizeThreshold: 5242880,
  300. AccessStatHistoryWeight: 0.8,
  301. }
  302. cfg.Mount = &mntcfg.Config{
  303. Enabled: false,
  304. AttrTimeout: time.Second * 10,
  305. UploadPendingTime: time.Second * 30,
  306. CacheActiveTime: time.Minute * 1,
  307. CacheExpireTime: time.Minute * 1,
  308. ScanDataDirInterval: time.Minute * 10,
  309. }
  310. cfg.PubLock = publock.Config{
  311. LeaseExpiredSeconds: 5,
  312. }
  313. configData, err := json.MarshalIndent(cfg, "", " ")
  314. if err != nil {
  315. return fmt.Errorf("序列化配置失败: %w", err)
  316. }
  317. configData = append(configData, '\n')
  318. err = os.WriteFile(configPath, configData, 0644)
  319. if err != nil {
  320. return fmt.Errorf("写入配置文件失败: %w", err)
  321. }
  322. return nil
  323. }

本项目旨在将云际存储公共基础设施化,使个人及企业可低门槛使用高效的云际存储服务(安装开箱即用云际存储客户端即可,无需关注其他组件的部署),同时支持用户灵活便捷定制云际存储的功能细节。