From 55adf5142c1695a595ab2e4d0b5e4bbe33ba632f Mon Sep 17 00:00:00 2001 From: xzyaoi Date: Wed, 19 Dec 2018 14:20:55 +0800 Subject: [PATCH 1/4] add feat:init --- cli/config.go | 3 +++ cli/daemon.go | 4 ++++ cli/handler.go | 14 +++++++++++++- cli/repository.go | 6 ++++++ cli/webui.go | 6 ++++++ 5 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 cli/webui.go diff --git a/cli/config.go b/cli/config.go index 4bc5fd264..d0d684fee 100644 --- a/cli/config.go +++ b/cli/config.go @@ -125,6 +125,9 @@ func validateConfig() { // create logs folder logsFolder := filepath.Join(cvpmPath, "logs") createFolderIfNotExist(logsFolder) + // create webui folder + webuiFolder := filepath.Join(cvpmPath, "webui") + createFolderIfNotExist(webuiFolder) // check if system log file exists cvpmLogPath := filepath.Join(cvpmPath, "logs", "system.log") createFileIfNotExist(cvpmLogPath) diff --git a/cli/daemon.go b/cli/daemon.go index 4509106bd..8edd05b71 100644 --- a/cli/daemon.go +++ b/cli/daemon.go @@ -4,6 +4,7 @@ You can uninstall that service by using cvpm daemon uninstall */ package main import ( + "github.com/gin-contrib/static" "github.com/fatih/color" "github.com/gin-gonic/gin" "github.com/googollee/go-socket.io" @@ -154,6 +155,8 @@ func BeforeResponse() gin.HandlerFunc { /repos -> Get to fetch Running Repos */ func runServer(port string) { + config := readConfig() + webuiFolder := filepath.Join(config.Local.LocalFolder, "webui") color.Red("Initiating") var err error socketServer, err = socketio.NewServer(nil) @@ -163,6 +166,7 @@ func runServer(port string) { r := gin.Default() r.Use(BeforeResponse()) watchLogs(socketServer) + r.Use(static.Serve("/", static.LocalFile(webuiFolder, false))) // Status Related Handlers r.GET("/status", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ diff --git a/cli/handler.go b/cli/handler.go index 5f85b2bce..bf66f0366 100644 --- a/cli/handler.go +++ b/cli/handler.go @@ -9,6 +9,7 @@ package main import ( "bufio" "fmt" + "github.com/manifoldco/promptui" "github.com/fatih/color" "github.com/getsentry/raven-go" "github.com/mitchellh/go-homedir" @@ -52,6 +53,8 @@ func InstallHandler(c *cli.Context) { color.Cyan("Installing... Please wait patiently") pip([]string{"install", "cvpm", "--user"}) return + } else if remoteURL == "webui" { + InstallWebUi() } else { color.Cyan("Installing to " + localFolder) } @@ -87,6 +90,7 @@ func DaemonHandler(c *cli.Context) { } } +// Handle Repo Related Command func RepoHandler(c *cli.Context) { taskParams := c.Args().Get(0) switch taskParams { @@ -113,6 +117,7 @@ func RepoHandler(c *cli.Context) { } } +// Handle Config Related Command func ConfigHandler(c *cli.Context) { homepath, _ := homedir.Dir() configFilePath := filepath.Join(homepath, "cvpm", "config.toml") @@ -159,5 +164,12 @@ func ConfigHandler(c *cli.Context) { } func InitHandler(c *cli.Context) { - + prompt := promptui.Prompt{ + Label: "String", + } + result, err := prompt.Run() + if err != nil { + panic(err) + } + InitNewRepo(result) } diff --git a/cli/repository.go b/cli/repository.go index e5436bbad..05d16a249 100644 --- a/cli/repository.go +++ b/cli/repository.go @@ -192,3 +192,9 @@ func InstallFromGit(remoteURL string) { writeConfig(config) PostInstallation(repoFolder) } + +// Init a new repoo by using bolierplate +func InitNewRepo (repoName string) { + bolierplateURL := "https://github.com/cvmodel/bolierplate.git" + CloneFromGit(bolierplateURL, repoName) +} diff --git a/cli/webui.go b/cli/webui.go new file mode 100644 index 000000000..7cd6104f4 --- /dev/null +++ b/cli/webui.go @@ -0,0 +1,6 @@ +package main + +// Install Web UI -> Download Latest and Unzip +func InstallWebUi () { + +} From 22e1625f5ec8bca33a95b9b6c3364abec50a969a Mon Sep 17 00:00:00 2001 From: Xiaozhe Yao Date: Wed, 19 Dec 2018 14:34:00 +0800 Subject: [PATCH 2/4] fix clone repo problem --- cli/repository.go | 46 +++++++++++++------------------- cli/utils.go | 12 +++++++++ dashboard/config/index.js | 2 +- dashboard/src/services/config.js | 2 +- docs/.vuepress/config.js | 1 + docs/en-US/guide/developer.md | 1 + docs/zh-CN/guide/developer.md | 1 + 7 files changed, 35 insertions(+), 30 deletions(-) create mode 100644 docs/en-US/guide/developer.md create mode 100644 docs/zh-CN/guide/developer.md diff --git a/cli/repository.go b/cli/repository.go index 05d16a249..08dace177 100644 --- a/cli/repository.go +++ b/cli/repository.go @@ -6,7 +6,6 @@ import ( "github.com/fatih/color" "github.com/getsentry/raven-go" "gopkg.in/src-d/go-git.v4" - "io/ioutil" "log" "os" "path/filepath" @@ -101,12 +100,8 @@ func runRepo(Vendor string, Name string, Solver string, Port string) { } func CloneFromGit(remoteURL string, targetFolder string) Repository { - localFolderName := strings.Split(remoteURL, "/") - vendorName := localFolderName[len(localFolderName)-2] - repoName := localFolderName[len(localFolderName)-1] - localFolder := filepath.Join(targetFolder, vendorName, repoName) - color.Cyan("Cloning " + remoteURL + " into " + localFolder) - _, err := git.PlainClone(localFolder, false, &git.CloneOptions{ + color.Cyan("Cloning " + remoteURL + " into " + targetFolder) + _, err := git.PlainClone(targetFolder, false, &git.CloneOptions{ URL: remoteURL, Progress: os.Stdout, }) @@ -114,7 +109,7 @@ func CloneFromGit(remoteURL string, targetFolder string) Repository { raven.CaptureErrorAndWait(err, nil) fmt.Println(err) } - repo := Repository{Name: repoName, Vendor: vendorName, LocalFolder: localFolder} + repo := Repository{Name: repoName, Vendor: vendorName, LocalFolder: targetFolder} return repo } @@ -149,30 +144,19 @@ func PostInstallation(repoFolder string) { } // Return Repository Meta Info: Dependency, Config, Disk Size and Readme + func GetMetaInfo(Vendor string, Name string) RepositoryMetaInfo { repos := readRepos() repositoryMeta := RepositoryMetaInfo{} for _, existed_repo := range repos { if existed_repo.Name == Name && existed_repo.Vendor == Vendor { // Read config file etc - byte_config, err := ioutil.ReadFile(filepath.Join(existed_repo.LocalFolder, "cvpm.toml")) - if err != nil { - repositoryMeta.Config = "Read cvpm.toml failed" - } else { - repositoryMeta.Config = string(byte_config) - } - byte_dependency, err := ioutil.ReadFile(filepath.Join(existed_repo.LocalFolder, "requirements.txt")) - if err != nil { - repositoryMeta.Dependency = "Read requirements.txt failed" - } else { - repositoryMeta.Dependency = string(byte_dependency) - } - byte_readme, err := ioutil.ReadFile(filepath.Join(existed_repo.LocalFolder, "README.md")) - if err != nil { - repositoryMeta.Readme = "Read Readme.md failed" - } else { - repositoryMeta.Readme = string(byte_readme) - } + readmeFilePath := filepath.Join(existed_repo.LocalFolder, "README.md") + cvpmConfigFilePath := filepath.Join(existed_repo.LocalFolder, "cvpm.toml") + requirementsFilePath := filepath.Join(existed_repo.LocalFolder, "requirements.txt") + repositoryMeta.Config = readFileContent(cvpmConfigFilePath) + repositoryMeta.Dependency = readFileContent(requirementsFilePath) + repositoryMeta.Readme = readFileContent(readmeFilePath) packageSize := getDirSizeMB(existed_repo.LocalFolder) repositoryMeta.DiskSize = packageSize } @@ -184,7 +168,13 @@ func GetMetaInfo(Vendor string, Name string) RepositoryMetaInfo { func InstallFromGit(remoteURL string) { config := readConfig() var repo Repository - repo = CloneFromGit(remoteURL, config.Local.LocalFolder) + // prepare local folder + localFolderName := strings.Split(remoteURL, "/") + vendorName := localFolderName[len(localFolderName)-2] + repoName := localFolderName[len(localFolderName)-1] + localFolder := filepath.Join(config.Local.LocalFolder, vendorName, repoName) + + repo = CloneFromGit(remoteURL, localFolder) repoFolder := repo.LocalFolder InstallDependencies(repoFolder) GeneratingRunners(repoFolder) @@ -196,5 +186,5 @@ func InstallFromGit(remoteURL string) { // Init a new repoo by using bolierplate func InitNewRepo (repoName string) { bolierplateURL := "https://github.com/cvmodel/bolierplate.git" - CloneFromGit(bolierplateURL, repoName) + CloneFromGit(bolierplateURL, repoName) } diff --git a/cli/utils.go b/cli/utils.go index 86c113946..04c7fa8e4 100644 --- a/cli/utils.go +++ b/cli/utils.go @@ -4,6 +4,7 @@ import ( "net" "os" "os/user" + "io/ioutil" "path/filepath" "strconv" "time" @@ -76,3 +77,14 @@ func findNextOpenPort(port int) string { } return strPort } + +func readFileContent(filepath) string { + var content string + byte_content, err := ioutil.ReadFile(filepath) + if err != nil { + content = "Read " + filepath + "Failed!" + } else { + content = string(byte_content) + } + return content +} \ No newline at end of file diff --git a/dashboard/config/index.js b/dashboard/config/index.js index 591a543d4..094802e32 100644 --- a/dashboard/config/index.js +++ b/dashboard/config/index.js @@ -13,7 +13,7 @@ module.exports = { proxyTable: {}, // Various Dev Server settings - host: 'localhost', // can be overwritten by process.env.HOST + host: '0.0.0.0', // can be overwritten by process.env.HOST port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined autoOpenBrowser: false, errorOverlay: true, diff --git a/dashboard/src/services/config.js b/dashboard/src/services/config.js index 1bb494bbc..0d604476c 100644 --- a/dashboard/src/services/config.js +++ b/dashboard/src/services/config.js @@ -30,7 +30,7 @@ class ConfigService { const configService = new ConfigService() const discoveryConfig = { - 'endpoint': 'http://127.0.0.1:3000' + 'endpoint': 'http://192.168.1.4:3000' } export { diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 4dd3d100c..3e4d612b9 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -126,6 +126,7 @@ function genSidebarConfig(title) { 'getting-started', 'install-package', 'write-package', + 'developer', 'discovery', 'contributing', 'credits', diff --git a/docs/en-US/guide/developer.md b/docs/en-US/guide/developer.md new file mode 100644 index 000000000..6f8d214d2 --- /dev/null +++ b/docs/en-US/guide/developer.md @@ -0,0 +1 @@ +# Developer Guide \ No newline at end of file diff --git a/docs/zh-CN/guide/developer.md b/docs/zh-CN/guide/developer.md new file mode 100644 index 000000000..e26279adf --- /dev/null +++ b/docs/zh-CN/guide/developer.md @@ -0,0 +1 @@ +# 开发指南 \ No newline at end of file From bee0c18896e95fecf7fcceed845117c2c7aba6bf Mon Sep 17 00:00:00 2001 From: xzyaoi Date: Wed, 19 Dec 2018 15:02:50 +0800 Subject: [PATCH 3/4] add feat/init --- cli/daemon.go | 2 +- cli/handler.go | 61 +++++++++++++++++++++++++++++++++-------------- cli/repository.go | 14 +++++------ cli/utils.go | 10 ++++---- cli/webui.go | 2 +- 5 files changed, 57 insertions(+), 32 deletions(-) diff --git a/cli/daemon.go b/cli/daemon.go index 8edd05b71..51fbd596c 100644 --- a/cli/daemon.go +++ b/cli/daemon.go @@ -4,8 +4,8 @@ You can uninstall that service by using cvpm daemon uninstall */ package main import ( - "github.com/gin-contrib/static" "github.com/fatih/color" + "github.com/gin-contrib/static" "github.com/gin-gonic/gin" "github.com/googollee/go-socket.io" "github.com/hpcloud/tail" diff --git a/cli/handler.go b/cli/handler.go index bf66f0366..4709eac30 100644 --- a/cli/handler.go +++ b/cli/handler.go @@ -7,11 +7,12 @@ and etc. */ package main import ( + "errors" "bufio" "fmt" - "github.com/manifoldco/promptui" "github.com/fatih/color" "github.com/getsentry/raven-go" + "github.com/manifoldco/promptui" "github.com/mitchellh/go-homedir" "github.com/olekukonko/tablewriter" "github.com/urfave/cli" @@ -54,7 +55,7 @@ func InstallHandler(c *cli.Context) { pip([]string{"install", "cvpm", "--user"}) return } else if remoteURL == "webui" { - InstallWebUi() + InstallWebUi() } else { color.Cyan("Installing to " + localFolder) } @@ -112,12 +113,29 @@ func RepoHandler(c *cli.Context) { case "ps": requestParams := map[string]string{} ClientGet("repos", requestParams) + case "init": + InitHandler(c) default: color.Red("Command Not Supported!") } } // Handle Config Related Command + +// validate if python/pip/others exists or does not change +func validateIfProgramAllowed(rawInput string) error { + input := strings.TrimSpace(rawInput) + if input == "y" || input == "Y" || input == "Yes" || input == "" { + return nil + } else { + if _, err := os.Stat(input); os.IsNotExist(err) { + return errors.New(input + " not exists") + } else { + return errors.New("Unknown Error") + } + } +} + func ConfigHandler(c *cli.Context) { homepath, _ := homedir.Dir() configFilePath := filepath.Join(homepath, "cvpm", "config.toml") @@ -136,28 +154,35 @@ func ConfigHandler(c *cli.Context) { var nextConfig cvpmConfig nextConfig.Local.LocalFolder = prevConfig.Local.LocalFolder // Handle Python Location - reader := bufio.NewReader(os.Stdin) - fmt.Printf("Python Location[" + prevConfig.Local.Python + "]") - newPyLocation, _ := reader.ReadString('\n') - newPyLocation = strings.TrimSpace(newPyLocation) + promptPy := promptui.Prompt{ + Label: "Python(3) Path", + Validate: validateIfProgramAllowed, + } + fmt.Printf("Original Python Location [" + prevConfig.Local.Python + "]") + result, err := promptPy.Run() + if err != nil { + fmt.Printf("%v\n", err) + return + } + newPyLocation := strings.TrimSpace(result) if newPyLocation == "y" || newPyLocation == "Y" || newPyLocation == "Yes" || newPyLocation == "" { newPyLocation = prevConfig.Local.Python - } else { - if _, err := os.Stat(newPyLocation); os.IsNotExist(err) { - log.Fatal("Python executable file not found: No such file") - } } nextConfig.Local.Python = newPyLocation // Handle Pypi Location - fmt.Printf("Pip Location[" + prevConfig.Local.Pip + "]") - newPipLocation, _ := reader.ReadString('\n') - newPipLocation = strings.TrimSpace(newPipLocation) + fmt.Printf("Original Pip Location [" + prevConfig.Local.Pip + "]") + promptPip := promptui.Prompt{ + Label: "Pip(3) Path", + Validate: validateIfProgramAllowed, + } + result, err = promptPip.Run() + if err != nil { + fmt.Printf("%v\n", err) + return + } + newPipLocation := strings.TrimSpace(result) if newPipLocation == "y" || newPipLocation == "Y" || newPipLocation == "Yes" || newPipLocation == "" { newPipLocation = prevConfig.Local.Pip - } else { - if _, err := os.Stat(newPipLocation); os.IsNotExist(err) { - log.Fatal("Pip executable file not found: No such file") - } } nextConfig.Local.Pip = newPipLocation writeConfig(nextConfig) @@ -165,7 +190,7 @@ func ConfigHandler(c *cli.Context) { func InitHandler(c *cli.Context) { prompt := promptui.Prompt{ - Label: "String", + Label: "Your Package Name", } result, err := prompt.Run() if err != nil { diff --git a/cli/repository.go b/cli/repository.go index 08dace177..fc1cbbedc 100644 --- a/cli/repository.go +++ b/cli/repository.go @@ -6,6 +6,7 @@ import ( "github.com/fatih/color" "github.com/getsentry/raven-go" "gopkg.in/src-d/go-git.v4" + "io/ioutil" "log" "os" "path/filepath" @@ -99,7 +100,7 @@ func runRepo(Vendor string, Name string, Solver string, Port string) { } } -func CloneFromGit(remoteURL string, targetFolder string) Repository { +func CloneFromGit(remoteURL string, targetFolder string) { color.Cyan("Cloning " + remoteURL + " into " + targetFolder) _, err := git.PlainClone(targetFolder, false, &git.CloneOptions{ URL: remoteURL, @@ -109,8 +110,6 @@ func CloneFromGit(remoteURL string, targetFolder string) Repository { raven.CaptureErrorAndWait(err, nil) fmt.Println(err) } - repo := Repository{Name: repoName, Vendor: vendorName, LocalFolder: targetFolder} - return repo } func InstallDependencies(localFolder string) { @@ -173,8 +172,9 @@ func InstallFromGit(remoteURL string) { vendorName := localFolderName[len(localFolderName)-2] repoName := localFolderName[len(localFolderName)-1] localFolder := filepath.Join(config.Local.LocalFolder, vendorName, repoName) + CloneFromGit(remoteURL, localFolder) + repo = Repository{Name: repoName, Vendor: vendorName, LocalFolder: localFolder} - repo = CloneFromGit(remoteURL, localFolder) repoFolder := repo.LocalFolder InstallDependencies(repoFolder) GeneratingRunners(repoFolder) @@ -184,7 +184,7 @@ func InstallFromGit(remoteURL string) { } // Init a new repoo by using bolierplate -func InitNewRepo (repoName string) { - bolierplateURL := "https://github.com/cvmodel/bolierplate.git" - CloneFromGit(bolierplateURL, repoName) +func InitNewRepo(repoName string) { + bolierplateURL := "https://github.com/cvmodel/bolierplate.git" + CloneFromGit(bolierplateURL, repoName) } diff --git a/cli/utils.go b/cli/utils.go index 04c7fa8e4..c4f087622 100644 --- a/cli/utils.go +++ b/cli/utils.go @@ -1,10 +1,10 @@ package main import ( + "io/ioutil" "net" "os" "os/user" - "io/ioutil" "path/filepath" "strconv" "time" @@ -78,13 +78,13 @@ func findNextOpenPort(port int) string { return strPort } -func readFileContent(filepath) string { +func readFileContent(filename string) string { var content string - byte_content, err := ioutil.ReadFile(filepath) + byte_content, err := ioutil.ReadFile(filename) if err != nil { - content = "Read " + filepath + "Failed!" + content = "Read " + filename + "Failed!" } else { content = string(byte_content) } return content -} \ No newline at end of file +} diff --git a/cli/webui.go b/cli/webui.go index 7cd6104f4..006f97843 100644 --- a/cli/webui.go +++ b/cli/webui.go @@ -1,6 +1,6 @@ package main // Install Web UI -> Download Latest and Unzip -func InstallWebUi () { +func InstallWebUi() { } From 0c2b09fbf05b463353afe85efe0b3c7f80cd9725 Mon Sep 17 00:00:00 2001 From: xzyaoi Date: Wed, 19 Dec 2018 16:13:00 +0800 Subject: [PATCH 4/4] finish feat/init --- cli/config.go | 1 - cli/handler.go | 53 +++++++++++++++++++++++++++----------------------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/cli/config.go b/cli/config.go index d0d684fee..bcc43f314 100644 --- a/cli/config.go +++ b/cli/config.go @@ -115,7 +115,6 @@ func createFileIfNotExist(filePath string) { func validateConfig() { if !isRoot() { homepath := getHomeDir() - log.Println(homepath) // Validate CVPM Path cvpmPath := filepath.Join(homepath, "cvpm") createFolderIfNotExist(cvpmPath) diff --git a/cli/handler.go b/cli/handler.go index 4709eac30..a0a3cfc51 100644 --- a/cli/handler.go +++ b/cli/handler.go @@ -7,8 +7,8 @@ and etc. */ package main import ( - "errors" "bufio" + "errors" "fmt" "github.com/fatih/color" "github.com/getsentry/raven-go" @@ -123,6 +123,7 @@ func RepoHandler(c *cli.Context) { // Handle Config Related Command // validate if python/pip/others exists or does not change + func validateIfProgramAllowed(rawInput string) error { input := strings.TrimSpace(rawInput) if input == "y" || input == "Y" || input == "Yes" || input == "" { @@ -131,11 +132,25 @@ func validateIfProgramAllowed(rawInput string) error { if _, err := os.Stat(input); os.IsNotExist(err) { return errors.New(input + " not exists") } else { - return errors.New("Unknown Error") + return nil } } } +// trigger and parse input filepath +func InputAndParseConfigContent(label string, validate promptui.ValidateFunc) string { + prompt := promptui.Prompt{ + Label: label, + Validate: validate, + } + result, err := prompt.Run() + if err != nil { + fmt.Printf("%v\n", err) + return "" + } + return result +} + func ConfigHandler(c *cli.Context) { homepath, _ := homedir.Dir() configFilePath := filepath.Join(homepath, "cvpm", "config.toml") @@ -154,33 +169,17 @@ func ConfigHandler(c *cli.Context) { var nextConfig cvpmConfig nextConfig.Local.LocalFolder = prevConfig.Local.LocalFolder // Handle Python Location - promptPy := promptui.Prompt{ - Label: "Python(3) Path", - Validate: validateIfProgramAllowed, - } - fmt.Printf("Original Python Location [" + prevConfig.Local.Python + "]") - result, err := promptPy.Run() - if err != nil { - fmt.Printf("%v\n", err) - return - } - newPyLocation := strings.TrimSpace(result) + fmt.Println("Original Python Location: " + prevConfig.Local.Python) + newPyLocation := InputAndParseConfigContent("Python(3)", validateIfProgramAllowed) + newPyLocation = strings.TrimSpace(newPyLocation) if newPyLocation == "y" || newPyLocation == "Y" || newPyLocation == "Yes" || newPyLocation == "" { newPyLocation = prevConfig.Local.Python } nextConfig.Local.Python = newPyLocation // Handle Pypi Location - fmt.Printf("Original Pip Location [" + prevConfig.Local.Pip + "]") - promptPip := promptui.Prompt{ - Label: "Pip(3) Path", - Validate: validateIfProgramAllowed, - } - result, err = promptPip.Run() - if err != nil { - fmt.Printf("%v\n", err) - return - } - newPipLocation := strings.TrimSpace(result) + fmt.Println("Original Pip Location: " + prevConfig.Local.Pip) + newPipLocation := InputAndParseConfigContent("Pip(3)", validateIfProgramAllowed) + newPipLocation = strings.TrimSpace(newPipLocation) if newPipLocation == "y" || newPipLocation == "Y" || newPipLocation == "Yes" || newPipLocation == "" { newPipLocation = prevConfig.Local.Pip } @@ -197,4 +196,10 @@ func InitHandler(c *cli.Context) { panic(err) } InitNewRepo(result) + // rename {package_name} to real package name + pyFolderName := filepath.Join(result, "{package_name}") + err = os.Rename(pyFolderName, filepath.Join(result, result)) + if err != nil { + fmt.Println(err) + } }