diff --git a/arduino/dir.go b/arduino/dir.go new file mode 100644 index 00000000..923c8f07 --- /dev/null +++ b/arduino/dir.go @@ -0,0 +1,52 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package arduino + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + + "github.com/arduino/go-paths-helper" + "github.com/arduino/go-win32-utils" +) + +// DataDir returns the Arduino default data directory (arduino15) +func DataDir() (*paths.Path, error) { + userHomeDir, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("unable to get user home dir: %w", err) + } + + var path *paths.Path + switch runtime.GOOS { + case "darwin": + path = paths.New(filepath.Join(userHomeDir, "Library", "Arduino15")) + case "windows": + localAppDataPath, err := win32.GetLocalAppDataFolder() + if err != nil { + return nil, fmt.Errorf("unable to get local app data folder: %w", err) + } + path = paths.New(filepath.Join(localAppDataPath, "Arduino15")) + default: // linux, android, *bsd, plan9 and other Unix-like systems + path = paths.New(filepath.Join(userHomeDir, ".arduino15")) + } + + return path, nil +} diff --git a/cli/config/init.go b/cli/config/init.go index 1ab8b58b..3002a7ee 100644 --- a/cli/config/init.go +++ b/cli/config/init.go @@ -18,13 +18,25 @@ package config import ( + "errors" + "fmt" "os" + "strings" "github.com/arduino/arduino-cli/cli/errorcodes" "github.com/arduino/arduino-cli/cli/feedback" - "github.com/arduino/arduino-cloud-cli/command/config" + "github.com/arduino/arduino-cloud-cli/arduino" + "github.com/arduino/arduino-cloud-cli/internal/config" + "github.com/arduino/go-paths-helper" + "github.com/manifoldco/promptui" "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +const ( + clientIDLen = 32 + clientSecretLen = 64 ) var initFlags struct { @@ -36,32 +48,105 @@ var initFlags struct { func initInitCommand() *cobra.Command { initCommand := &cobra.Command{ Use: "init", - Short: "Initialize a configuration file with default values", - Long: "Initialize an Arduino IoT Cloud CLI configuration file with default values", + Short: "Initialize a configuration file", + Long: "Initialize an Arduino IoT Cloud CLI configuration", Run: runInitCommand, } - initCommand.Flags().StringVar(&initFlags.destDir, "dest-dir", ".", "Sets where to save the configuration file.") - initCommand.Flags().BoolVar(&initFlags.overwrite, "overwrite", false, "Overwrite existing config file.") + initCommand.Flags().StringVar(&initFlags.destDir, "dest-dir", "", "Sets where to save the configuration file") + initCommand.Flags().BoolVar(&initFlags.overwrite, "overwrite", false, "Overwrite existing config file") initCommand.Flags().StringVar(&initFlags.format, "config-format", "yaml", "Format of the configuration file, can be {yaml|json}") return initCommand } func runInitCommand(cmd *cobra.Command, args []string) { - logrus.Infof("Initializing a config file in folder: %s", initFlags.destDir) + logrus.Info("Initializing config file") + + // Get default destination directory if it's not passed + if initFlags.destDir == "" { + configPath, err := arduino.DataDir() + if err != nil { + feedback.Errorf("Error during config init: cannot retrieve arduino default directory: %v", err) + os.Exit(errorcodes.ErrGeneric) + } + initFlags.destDir = configPath.String() + } + + // Validate format flag + initFlags.format = strings.ToLower(initFlags.format) + if initFlags.format != "json" && initFlags.format != "yaml" { + feedback.Error("Error during config init: format is not valid, provide 'json' or 'yaml'") + os.Exit(errorcodes.ErrGeneric) + } - params := &config.InitParams{ - DestDir: initFlags.destDir, - Overwrite: initFlags.overwrite, - Format: initFlags.format, + // Check that the destination directory is valid and build the configuration file path + configPath, err := paths.New(initFlags.destDir).Abs() + if err != nil { + feedback.Errorf("Error during config init: cannot retrieve absolute path of %s: %v", initFlags.destDir, err) + os.Exit(errorcodes.ErrGeneric) + } + if !configPath.IsDir() { + feedback.Errorf("Error during config init: %s is not a valid directory", configPath) + os.Exit(errorcodes.ErrGeneric) + } + configFile := configPath.Join(config.Filename + "." + initFlags.format) + if !initFlags.overwrite && configFile.Exist() { + feedback.Errorf("Error during config init: %s already exists, use '--overwrite' to overwrite it", + configFile) + os.Exit(errorcodes.ErrGeneric) } - err := config.Init(params) + // Take needed configuration parameters starting an interactive mode + feedback.Print("To obtain your API credentials visit https://create.arduino.cc/iot/integrations") + id, key, err := paramsPrompt() if err != nil { - feedback.Errorf("Error during config init: %v", err) + feedback.Errorf("Error during config init: cannot take config params: %v", err) os.Exit(errorcodes.ErrGeneric) } - logrus.Info("Config file successfully initialized") + // Write the configuration file + newSettings := viper.New() + newSettings.SetConfigPermissions(os.FileMode(0600)) + newSettings.Set("client", id) + newSettings.Set("secret", key) + if err := newSettings.WriteConfigAs(configFile.String()); err != nil { + feedback.Errorf("Error during config init: cannot create config file: %v", err) + os.Exit(errorcodes.ErrGeneric) + } + + feedback.Printf("Config file successfully initialized at: %s", configFile) +} + +func paramsPrompt() (id, key string, err error) { + prompt := promptui.Prompt{ + Label: "Please enter the Client ID", + Validate: func(s string) error { + if len(s) != clientIDLen { + return errors.New("client-id not valid") + } + return nil + }, + } + id, err = prompt.Run() + if err != nil { + return "", "", fmt.Errorf("client prompt fail: %w", err) + } + + prompt = promptui.Prompt{ + Mask: '*', + Label: "Please enter the Client Secret", + Validate: func(s string) error { + if len(s) != clientSecretLen { + return errors.New("client secret not valid") + } + return nil + }, + } + key, err = prompt.Run() + if err != nil { + return "", "", fmt.Errorf("client secret prompt fail: %w", err) + } + + return id, key, nil } diff --git a/command/config/init.go b/command/config/init.go deleted file mode 100644 index 4a350eac..00000000 --- a/command/config/init.go +++ /dev/null @@ -1,76 +0,0 @@ -// This file is part of arduino-cloud-cli. -// -// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package config - -import ( - "errors" - "fmt" - "os" - "strings" - - "github.com/arduino/arduino-cloud-cli/internal/config" - "github.com/arduino/go-paths-helper" - "github.com/spf13/viper" -) - -// InitParams contains the parameters needed to initialize a configuration file. -type InitParams struct { - DestDir string // Destination directory in which the configuration file will be saved - Overwrite bool // Overwrite specifies if existing config file should be overwritten - Format string // Config file format, can be 'json' or 'yaml' -} - -func validateFormatString(arg string) error { - if arg != "json" && arg != "yaml" { - return errors.New("passed format is not valid, select between 'json' and 'yaml'") - } - return nil -} - -// Init initializes a configuration file with default values. -// If the file doesn't exist, it is created. -// If it exists, it is written to only if overwrite param is true. -func Init(params *InitParams) error { - configPath, err := paths.New(params.DestDir).Abs() - if err != nil { - return fmt.Errorf("%s: %w", "cannot retrieve absolute path of passed dest-dir", err) - } - if !configPath.IsDir() { - return fmt.Errorf("%s: %w", "passed dest-dir is not a valid directory", err) - } - - params.Format = strings.ToLower(params.Format) - if err := validateFormatString(params.Format); err != nil { - return err - } - - configFile := configPath.Join(config.Filename + "." + params.Format) - - if !params.Overwrite && configFile.Exist() { - return errors.New("config file already exists, use --overwrite to discard the existing one") - } - - newSettings := viper.New() - newSettings.SetConfigPermissions(os.FileMode(0600)) - config.SetDefaults(newSettings) - if err := newSettings.WriteConfigAs(configFile.String()); err != nil { - return fmt.Errorf("cannot create config file: %v", err) - } - - return nil -} diff --git a/go.mod b/go.mod index 640d62f2..116de811 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,13 @@ require ( github.com/antihax/optional v1.0.0 github.com/arduino/arduino-cli v0.0.0-20210607095659-16f41352eac3 github.com/arduino/go-paths-helper v1.6.1 + github.com/arduino/go-win32-utils v0.0.0-20180330194947-ed041402e83b github.com/arduino/iot-client-go v1.3.4-0.20211116175324-9a98dd4ad269 github.com/gofrs/uuid v4.0.0+incompatible github.com/google/go-cmp v0.5.6 github.com/howeyc/crc16 v0.0.0-20171223171357-2b2a61e366a6 github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5 + github.com/manifoldco/promptui v0.9.0 github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.1.3 github.com/spf13/viper v1.7.0 diff --git a/go.sum b/go.sum index 900ab731..1a432fc8 100644 --- a/go.sum +++ b/go.sum @@ -60,8 +60,6 @@ github.com/arduino/go-properties-orderedmap v1.3.0/go.mod h1:DKjD2VXY/NZmlingh4l github.com/arduino/go-timeutils v0.0.0-20171220113728-d1dd9e313b1b/go.mod h1:uwGy5PpN4lqW97FiLnbcx+xx8jly5YuPMJWfVwwjJiQ= github.com/arduino/go-win32-utils v0.0.0-20180330194947-ed041402e83b h1:3PjgYG5gVPA7cipp7vIR2lF96KkEJIFBJ+ANnuv6J20= github.com/arduino/go-win32-utils v0.0.0-20180330194947-ed041402e83b/go.mod h1:iIPnclBMYm1g32Q5kXoqng4jLhMStReIP7ZxaoUC2y8= -github.com/arduino/iot-client-go v1.3.4-0.20211103115604-d4d372164262 h1:qVq8cdkaRPaLc9DAjY/6rH3ocs6ZvnEJtD26f5++/RU= -github.com/arduino/iot-client-go v1.3.4-0.20211103115604-d4d372164262/go.mod h1:gYvpMt7Qw+OSScTLyIlCnpbvy9y96ey/2zhB4w6FoK0= github.com/arduino/iot-client-go v1.3.4-0.20211116175324-9a98dd4ad269 h1:01RHB48b1QRN5viXtdtmSkvyudxHqczDV+OD+1PuE6M= github.com/arduino/iot-client-go v1.3.4-0.20211116175324-9a98dd4ad269/go.mod h1:gYvpMt7Qw+OSScTLyIlCnpbvy9y96ey/2zhB4w6FoK0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -76,8 +74,11 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cmaglie/go.rice v1.0.3 h1:ZBLmBdQp6ejc+n8eMNH0uuRSKkg6kKe6ORjXKnyHBYw= @@ -272,6 +273,8 @@ github.com/leonelquinteros/gotext v1.4.0/go.mod h1:yZGXREmoGTtBvZHNcc+Yfug49G/2s github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/marcinbor85/gohex v0.0.0-20210308104911-55fb1c624d84/go.mod h1:Pb6XcsXyropB9LNHhnqaknG/vEwYztLkQzVCHv8sQ3M= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= @@ -499,8 +502,6 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20211101193420-4a448f8816b3 h1:VrJZAjbekhoRn7n5FBujY31gboH+iB3pdLxn3gE9FjU= -golang.org/x/net v0.0.0-20211101193420-4a448f8816b3/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -508,8 +509,6 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20211028175245-ba495a64dcb5 h1:v79phzBz03tsVCUTbvTBmmC3CUXF5mKYt7DA4ZVldpM= -golang.org/x/oauth2 v0.0.0-20211028175245-ba495a64dcb5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -528,6 +527,7 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=