diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 571d451..796cba1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,12 +50,18 @@ jobs: run: | go test -v -cover ./cmd/ ./external/... - - name: Run GoReleaser + - name: Run GoReleaser (for Linux build and Syntax check) uses: goreleaser/goreleaser-action@v2 + env: + GOOS: linux + GOARCH: amd64 with: version: latest - args: check - env: - GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} - # GitHub sets this automatically - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + args: build --single-target --snapshot + + - name: 'Upload Artifact' + uses: actions/upload-artifact@v3 + with: + name: distribution + path: dist/epcc-cli_linux_amd64 + retention-days: 5 diff --git a/.gitignore b/.gitignore index e5aab7e..1d9fd40 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,4 @@ bin dist/ epcc-cli epcc -profiles .envrc \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 8db12d2..156a74b 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -14,7 +14,7 @@ builds: flags: - -trimpath ldflags: - - '-s -w -X version.version={{.Version}} -X version.commit={{.Commit}}' + - '-s -w -X github.com/elasticpath/epcc-cli/external/version.Version={{.Version}} -X github.com/elasticpath/epcc-cli/external/version.Commit={{.Commit}}' goos: - freebsd - windows diff --git a/README.md b/README.md index 568f4bb..a5d3dbd 100644 --- a/README.md +++ b/README.md @@ -202,4 +202,11 @@ INFO[0001] PUT https://api.moltin.com/v2/customers/8f720da2-37d1-41b7-94da-3fd35 7. Copying and pasting is terrible and as a result epcc-cli has a few ways of ameliorating the experience of working with ids. To update the customer without the id, you can use an alias `last_customer` (and this will auto complete). For example `epcc update customer last_customer name "Jonah Smith"` -STEVE WILL FIX + + + +## Development Tips + +```bash +git fetch --all --tags && reflex -v -r '(\.go$)|(resources.yaml|go.mod)$' -- sh -c "go build -ldflags=\"-X github.com/elasticpath/epcc-cli/external/version.Version=$(git describe --tags --abbrev=0)+1 -X github.com/elasticpath/epcc-cli/external/version.Commit=$(git rev-parse --short HEAD)-dirty\" -o ./epcc" +``` \ No newline at end of file diff --git a/cmd/commercemanager.go b/cmd/commercemanager.go index 959f553..15a222b 100644 --- a/cmd/commercemanager.go +++ b/cmd/commercemanager.go @@ -3,7 +3,7 @@ package cmd import ( "fmt" "github.com/elasticpath/epcc-cli/config" - "github.com/elasticpath/epcc-cli/shared" + "github.com/elasticpath/epcc-cli/external/browser" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "net/url" @@ -29,7 +29,7 @@ var cmCommand = &cobra.Command{ return fmt.Errorf("Don't know where Commerce Manager is for $EPCC_API_BASE_URL=%s \n", u) } - err = shared.OpenUrl(cmUrl) + err = browser.OpenUrl(cmUrl) if err != nil { return err } diff --git a/cmd/configure.go b/cmd/configure.go index eb768e5..8b35d20 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -17,7 +17,7 @@ var configure = &cobra.Command{ Long: "Will first prompt for a name then a series of variable specific for the user being created", Run: func(cmd *cobra.Command, args []string) { - configPath := profiles.GetProfilePath() + configPath := profiles.GetConfigFilePath() cfg, err := ini.Load(configPath) if err != nil { log.Errorf("error loading to file " + configPath) @@ -49,8 +49,6 @@ var configure = &cobra.Command{ os.Exit(1) } config.Envs = &newProfile - config.Profile = text - }, } diff --git a/cmd/docs.go b/cmd/docs.go index f561261..7d1e05a 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -2,9 +2,9 @@ package cmd import ( "fmt" + "github.com/elasticpath/epcc-cli/external/browser" "github.com/elasticpath/epcc-cli/external/completion" "github.com/elasticpath/epcc-cli/external/resources" - "github.com/elasticpath/epcc-cli/shared" "github.com/spf13/cobra" ) @@ -52,32 +52,32 @@ func openDoc(resourceDoc resources.Resource, verb string) error { if len(resourceDoc.Docs) < 1 { err = doDefault() } - err = shared.OpenUrl(resourceDoc.Docs) + err = browser.OpenUrl(resourceDoc.Docs) case "get-collection": if resourceDoc.GetCollectionInfo != nil && len(resourceDoc.GetCollectionInfo.Docs) < 1 { err = doDefault() } - err = shared.OpenUrl(resourceDoc.GetCollectionInfo.Docs) + err = browser.OpenUrl(resourceDoc.GetCollectionInfo.Docs) case "get": if resourceDoc.GetEntityInfo != nil && len(resourceDoc.GetEntityInfo.Docs) < 1 { err = doDefault() } - err = shared.OpenUrl(resourceDoc.GetEntityInfo.Docs) + err = browser.OpenUrl(resourceDoc.GetEntityInfo.Docs) case "update": if resourceDoc.UpdateEntityInfo != nil && len(resourceDoc.UpdateEntityInfo.Docs) < 1 { err = doDefault() } - err = shared.OpenUrl(resourceDoc.UpdateEntityInfo.Docs) + err = browser.OpenUrl(resourceDoc.UpdateEntityInfo.Docs) case "delete": if resourceDoc.DeleteEntityInfo != nil && len(resourceDoc.DeleteEntityInfo.Docs) < 1 { err = doDefault() } - err = shared.OpenUrl(resourceDoc.DeleteEntityInfo.Docs) + err = browser.OpenUrl(resourceDoc.DeleteEntityInfo.Docs) case "create": if resourceDoc.CreateEntityInfo != nil && len(resourceDoc.CreateEntityInfo.Docs) < 1 { err = doDefault() } - err = shared.OpenUrl(resourceDoc.CreateEntityInfo.Docs) + err = browser.OpenUrl(resourceDoc.CreateEntityInfo.Docs) default: return fmt.Errorf("Could not find verb %s", verb) diff --git a/cmd/logs.go b/cmd/logs.go index e354b5d..6f7170f 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -2,18 +2,16 @@ package cmd import ( "fmt" - "github.com/elasticpath/epcc-cli/shared" + "github.com/elasticpath/epcc-cli/external/profiles" "github.com/spf13/cobra" - "os" - "strings" + "strconv" ) var LogsClear = &cobra.Command{ Use: "clear", Short: "Clears all HTTP request and response logs", RunE: func(cmd *cobra.Command, args []string) error { - os.RemoveAll(shared.LogDirectory) - return nil + return profiles.ClearAllRequestLogs() }, } @@ -21,10 +19,13 @@ var LogsList = &cobra.Command{ Use: "list", Short: "List all HTTP logs", RunE: func(cmd *cobra.Command, args []string) error { - files := shared.AllFilesSortedByDate(shared.LogDirectory) - for i := 0; i < len(files); i++ { - name, _ := shared.Base64DecodeStripped(files[i].Name()) - fmt.Println(name) + files, err := profiles.GetAllRequestLogTitles() + if err != nil { + return err + } + + for idx, name := range files { + fmt.Printf("%d %s\n", idx, name) } return nil }, @@ -35,20 +36,21 @@ var LogsShow = &cobra.Command{ Short: "Show HTTP logs for specific number", Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - files := shared.AllFilesSortedByDate(shared.LogDirectory) - for i := 0; i < len(files); i++ { - name, _ := shared.Base64DecodeStripped(files[i].Name()) - segments := strings.Split(name, " ") - if segments[0] == args[0] { - content, err := os.ReadFile(shared.LogDirectory + "/" + files[i].Name()) - if err != nil { - return err - } - fmt.Print(string(content)) - break - } + + i, err := strconv.Atoi(args[0]) + + if err != nil { + return fmt.Errorf("Could not get the %s entry => %w", args[0], err) } + content, err := profiles.GetNthRequestLog(i) + + if err != nil { + return fmt.Errorf("Couldn't print logs: %v", err) + } + + fmt.Println(content) + return nil }, } diff --git a/cmd/root.go b/cmd/root.go index 62452cb..1b9dc97 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -51,7 +51,7 @@ func init() { RootCmd.PersistentFlags().BoolVarP(&json.MonochromeOutput, "monochrome-output", "M", false, "By default, epcc will output using colors if the terminal supports this. Use this option to disable it.") RootCmd.PersistentFlags().StringSliceVarP(&globals.RawHeaders, "header", "H", []string{}, "Extra headers and values to include in the request when sending HTTP to a server. You may specify any number of extra headers.") - RootCmd.PersistentFlags().StringVarP(&config.Profile, "profile", "P", "", "overrides the current EPCC_PROFILE var to run the command with the chosen profile.") + RootCmd.PersistentFlags().StringVarP(&profiles.ProfileName, "profile", "P", "default", "overrides the current EPCC_PROFILE var to run the command with the chosen profile.") aliasesCmd.AddCommand(aliasListCmd, aliasClearCmd) } @@ -92,7 +92,7 @@ Environment Variables }, SilenceUsage: true, - Version: fmt.Sprintf("EPCC CLI %s (Commit %s)", version.Version, version.Commit), + Version: fmt.Sprintf("%s (Commit %s)", version.Version, version.Commit), } func Execute() { @@ -103,16 +103,14 @@ func Execute() { } func initConfig() { - if config.Profile == "" { - envProfile, present := os.LookupEnv("EPCC_PROFILE") - if !present { - //creates configfile is this is users first time running app - profiles.GetProfilePath() - log.Println("profile tag and EPCC_PROFILE variable are absent") - return - } - config.Profile = envProfile + envProfileName, ok := os.LookupEnv("EPCC_PROFILE") + if ok { + profiles.ProfileName = envProfileName } - config.Envs = profiles.GetProfile(config.Profile) + config.Envs = profiles.GetProfile(profiles.ProfileName) + // Override profile configuration with environment variables + if err := env.Parse(config.Envs); err != nil { + panic("Could not parse environment variables") + } } diff --git a/config/config.go b/config/config.go index 6c591d2..f3e6aed 100644 --- a/config/config.go +++ b/config/config.go @@ -8,4 +8,3 @@ type Env struct { } var Envs = &Env{} -var Profile string diff --git a/external/aliases/aliases.go b/external/aliases/aliases.go index 77cda54..87fc8ae 100644 --- a/external/aliases/aliases.go +++ b/external/aliases/aliases.go @@ -19,7 +19,7 @@ import ( var filelock = sync.Mutex{} func GetAliasesForJsonApiType(jsonApiType string) map[string]string { - profileDirectory := profiles.GetProfileDirectory() + profileDirectory := profiles.GetProfileDataDirectory() aliasFile := getDirectoryForJsonApiType(profileDirectory, jsonApiType) aliasMap := map[string]string{} @@ -59,7 +59,7 @@ func SaveAliasesForResources(jsonTxt string) { log.Tracef("All aliases: %s", results) - profileDirectory := profiles.GetProfileDirectory() + profileDirectory := profiles.GetProfileDataDirectory() for resourceType, aliases := range results { saveAliasesForResource(profileDirectory, resourceType, aliases) } diff --git a/external/authentication/auth.go b/external/authentication/auth.go index 9ff1a78..fb49a58 100644 --- a/external/authentication/auth.go +++ b/external/authentication/auth.go @@ -5,9 +5,11 @@ import ( "encoding/json" "fmt" "github.com/elasticpath/epcc-cli/config" + "github.com/elasticpath/epcc-cli/external/profiles" "github.com/elasticpath/epcc-cli/external/version" "github.com/elasticpath/epcc-cli/globals" log "github.com/sirupsen/logrus" + "net/http/httputil" "os" "net/http" @@ -107,11 +109,19 @@ func auth() (string, error) { req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("User-Agent", fmt.Sprintf("epcc-cli/%s-%s", version.Version, version.Commit)) + dumpReq, err := httputil.DumpRequestOut(req, true) + if err != nil { + log.Errorf("error %v", err) + } + resp, err := HttpClient.Do(req) if err != nil { return "", err } + dumpRes, _ := httputil.DumpResponse(resp, true) + + profiles.LogRequestToDisk("POST", req.URL.Path, dumpReq, dumpRes, resp.StatusCode) if resp.StatusCode != 200 { return "", fmt.Errorf("error: unexpected status %s", resp.Status) } diff --git a/external/browser/browser.go b/external/browser/browser.go new file mode 100644 index 0000000..3b93670 --- /dev/null +++ b/external/browser/browser.go @@ -0,0 +1,22 @@ +package browser + +import ( + "fmt" + "os/exec" + "runtime" +) + +func OpenUrl(url string) error { + switch runtime.GOOS { + case "linux": + exec.Command("xdg-open", url).Start() + case "windows": + exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + exec.Command("open", url).Start() + default: + return fmt.Errorf("unsupported platform") + } + + return nil +} diff --git a/external/httpclient/httpclient.go b/external/httpclient/httpclient.go index 62eae85..bfe59d8 100644 --- a/external/httpclient/httpclient.go +++ b/external/httpclient/httpclient.go @@ -7,21 +7,17 @@ import ( "github.com/elasticpath/epcc-cli/config" "github.com/elasticpath/epcc-cli/external/authentication" "github.com/elasticpath/epcc-cli/external/json" + "github.com/elasticpath/epcc-cli/external/profiles" "github.com/elasticpath/epcc-cli/external/version" "github.com/elasticpath/epcc-cli/globals" - "github.com/elasticpath/epcc-cli/shared" log "github.com/sirupsen/logrus" "io" - "io/fs" "io/ioutil" "mime/multipart" "net/http" "net/http/httputil" "net/url" - "os" - "regexp" "runtime" - "strconv" "strings" "time" ) @@ -29,7 +25,6 @@ import ( var HttpClient = &http.Client{ Timeout: time.Second * 10, } -var SanitizeLogs = true func DoRequest(ctx context.Context, method string, path string, query string, payload io.Reader) (response *http.Response, error error) { return doRequestInternal(ctx, method, "application/json", path, query, payload) @@ -83,8 +78,17 @@ func doRequestInternal(ctx context.Context, method string, contentType string, p req.Header.Add("EP-Beta-Features", config.Envs.EPCC_BETA_API_FEATURES) } + dumpReq, err := httputil.DumpRequestOut(req, true) + if err != nil { + log.Error(err) + } + resp, err := HttpClient.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode >= 400 { if payload != nil { body, _ := ioutil.ReadAll(&bodyBuf) @@ -104,17 +108,13 @@ func doRequestInternal(ctx context.Context, method string, contentType string, p } else if resp.StatusCode >= 200 && resp.StatusCode <= 399 { log.Infof("%s %s ==> %s %s", method, reqURL.String(), resp.Proto, resp.Status) } - dumpReq, err := httputil.DumpRequestOut(req, true) - if err != nil { - log.Error(err) - } dumpRes, err := httputil.DumpResponse(resp, true) if err != nil { log.Error(err) } - logToDisk(method, path, dumpReq, dumpRes, resp.StatusCode) + profiles.LogRequestToDisk(method, path, dumpReq, dumpRes, resp.StatusCode) return resp, err } @@ -158,50 +158,3 @@ func AddHeaderByFlag(r *http.Request) error { } return nil } - -func logToDisk(requestMethod string, requestPath string, requestBytes []byte, responseBytes []byte, responseCode int) error { - os.Mkdir(shared.LogDirectory, os.ModePerm) - var logNumber = 1 - lastFile := getLastFile(shared.LogDirectory) - if lastFile != nil { - decodedFileNAme, err := shared.Base64DecodeStripped((*lastFile).Name()) - if err != nil { - return err - } - - fileNameParts := strings.Split(decodedFileNAme, " ") - logNumber, _ = strconv.Atoi(fileNameParts[0]) - logNumber++ - } - - filename := shared.Base64EncodeStripped(fmt.Sprintf("%d %s %s ==> %d", logNumber, requestMethod, requestPath, responseCode)) - f, err := os.Create(fmt.Sprintf("%s/%s", shared.LogDirectory, filename)) - if err != nil { - return err - } - defer f.Close() - - if SanitizeLogs { - regex1 := regexp.MustCompile(`(?i)client_id\s*[^A-Za-z0-9]\s*[A-Za-z0-9]*`) - redcatReq := regex1.ReplaceAllString(string(requestBytes[:]), "client_id: *****") - redactResp := regex1.ReplaceAllString(string(responseBytes[:]), "client_id: *****") - - f.Write([]byte(redcatReq)) - f.Write([]byte("\n")) - f.Write([]byte(redactResp)) - } else { - f.Write(requestBytes) - f.Write([]byte("\n")) - f.Write(responseBytes) - } - - return nil -} - -func getLastFile(logDirectory string) *fs.FileInfo { - all := shared.AllFilesSortedByDate(logDirectory) - if len(all) >= 1 { - return &all[len(all)-1] - } - return nil -} diff --git a/external/profiles/profiles.go b/external/profiles/profiles.go index c7dc244..ed61d9a 100644 --- a/external/profiles/profiles.go +++ b/external/profiles/profiles.go @@ -10,25 +10,38 @@ import ( //profile name is set to config.Profile in InitConfig +var ProfileName = "default" + func GetProfileDirectory() string { home, err := os.UserHomeDir() if err != nil { log.Errorf("could not get home directory") os.Exit(1) } - configDir := home + "/.epcc/profiles_data" - configDir = filepath.FromSlash(configDir) + profileDirectory := home + "/.epcc/" + profileDirectory = filepath.FromSlash(profileDirectory) + //built in check if dir exists + if err = os.MkdirAll(profileDirectory, 0700); err != nil { + log.Errorf("could not make directory") + } + + return profileDirectory +} + +func GetProfileDataDirectory() string { + profileDirectory := GetProfileDirectory() + profileDataDirectory := filepath.FromSlash(profileDirectory + "/" + ProfileName + "/data") //built in check if dir exists - if err = os.MkdirAll(configDir, 0700); err != nil { + if err := os.MkdirAll(profileDataDirectory, 0700); err != nil { log.Errorf("could not make directory") } - return configDir + return profileDataDirectory } -func GetProfilePath() string { +func GetConfigFilePath() string { configPath := GetProfileDirectory() - configPath = filepath.FromSlash(configPath + "/config") + configPath = filepath.Clean(filepath.FromSlash(configPath + "/../config")) if _, err := os.Stat(configPath); err != nil { log.Trace("could not find file at " + configPath) file, err := os.Create(configPath) @@ -43,18 +56,24 @@ func GetProfilePath() string { } func GetProfile(name string) *config.Env { - result := config.Env{} - configPath := GetProfilePath() + result := &config.Env{} + configPath := GetConfigFilePath() cfg, err := ini.Load(configPath) if err != nil { - log.Errorf("could not load file at " + configPath) - os.Exit(1) + log.Debug("could not load file at " + configPath) + return result } + if !cfg.HasSection(name) { - log.Errorf("could not find profile in file") - os.Exit(1) + log.Debug("could not find profile in file") + return result } - cfg.Section(name).MapTo(&result) - return &result + + err = cfg.Section(name).MapTo(result) + if err != nil { + log.Debug("could not load file at " + configPath) + } + + return result } diff --git a/external/profiles/requestlogs.go b/external/profiles/requestlogs.go new file mode 100644 index 0000000..a6798e3 --- /dev/null +++ b/external/profiles/requestlogs.go @@ -0,0 +1,164 @@ +package profiles + +import ( + b64 "encoding/base64" + "fmt" + "io/fs" + "io/ioutil" + "os" + "regexp" + "sort" + "strings" + "time" +) + +var SanitizeLogs = true + +func GetAllRequestLogTitles() ([]string, error) { + titles := make([]string, 0) + + files, err := allFilesSortedByDate() + + if err != nil { + return titles, err + } + for i := 0; i < len(files); i++ { + fname := strings.Split(files[i].Name(), "_") + + if len(fname) >= 2 { + name, _ := base64DecodeStripped(fname[1]) + titles = append(titles, files[i].ModTime().Format(time.Kitchen)+" "+name) + + } else { + titles = append(titles, files[i].Name()) + } + + } + + return titles, nil +} + +func GetNthRequestLog(n int) (string, error) { + + files, err := allFilesSortedByDate() + + if err != nil { + return "", err + } + + if n < 0 { + return "", fmt.Errorf("You must specify a positive integer log message to show") + } else if n >= len(files) { + return "", fmt.Errorf("There are only %d entries to show, cannot show entry: %d", len(files), n) + } + + dir, err := getRequestLogDirectory() + + if err != nil { + return "", err + } + + content, err := os.ReadFile(dir + "/" + files[n].Name()) + + if err != nil { + //Maybe a race condition, but maybe not. + return "", fmt.Errorf("Could not read entry %d, file exists(ed) but failed to read", n) + } + + return string(content), nil + +} + +func ClearAllRequestLogs() error { + dir, err := getRequestLogDirectory() + + if err != nil { + return err + } + + return os.RemoveAll(dir) +} + +func LogRequestToDisk(requestMethod string, requestPath string, requestBytes []byte, responseBytes []byte, responseCode int) error { + + if SanitizeLogs { + regex1 := regexp.MustCompile(`(?i)client_secret\s*[^A-Za-z0-9]\s*[A-Za-z0-9]*`) + requestBytes = regex1.ReplaceAll(requestBytes, []byte("client_secret=*****")) + responseBytes = regex1.ReplaceAll(responseBytes, []byte("client_secret=*****")) + } + + return SaveRequest(fmt.Sprintf("%s %s ==> %d", requestMethod, requestPath, responseCode), requestBytes, responseBytes) +} + +func SaveRequest(title string, requestBytes []byte, responseBytes []byte) error { + titleb64 := base64EncodeStripped(title) + + dir, err := getRequestLogDirectory() + + if err != nil { + return err + } + + f, err := os.Create(fmt.Sprintf("%s/%d_%s", dir, time.Now().Unix(), titleb64)) + if err != nil { + return err + } + + defer f.Close() + + _, err = f.Write(requestBytes) + if err != nil { + return err + } + _, err = f.Write([]byte("\n")) + if err != nil { + return err + } + _, err = f.Write(responseBytes) + if err != nil { + return err + } + + return nil +} + +func getRequestLogDirectory() (string, error) { + dir := GetProfileDataDirectory() + "/logs" + if err := os.MkdirAll(dir, 0700); err != nil { + return "", fmt.Errorf("Could not make directory %s", dir) + } + + return dir, nil +} + +func allFilesSortedByDate() ([]fs.FileInfo, error) { + dir, err := getRequestLogDirectory() + + if err != nil { + return nil, err + } + + files, err := ioutil.ReadDir(dir) + + if err != nil { + return nil, err + } + sort.Slice(files, func(i, j int) bool { + return files[i].ModTime().Before(files[j].ModTime()) + }) + + return files, nil +} + +func base64EncodeStripped(s string) string { + encoded := b64.URLEncoding.EncodeToString([]byte(s)) + return strings.TrimRight(encoded, "=") +} + +func base64DecodeStripped(s string) (string, error) { + if i := len(s) % 4; i != 0 { + s += strings.Repeat("=", 4-i) + } + decoded, err := b64.URLEncoding.DecodeString(s) + return string(decoded), err +} diff --git a/shared/share.go b/shared/share.go deleted file mode 100644 index 06a90cf..0000000 --- a/shared/share.go +++ /dev/null @@ -1,51 +0,0 @@ -package shared - -import ( - b64 "encoding/base64" - "fmt" - "io/fs" - "io/ioutil" - "os/exec" - "runtime" - "sort" - "strings" -) - -const LogDirectory = "profiles" - -func OpenUrl(cmUrl string) error { - switch runtime.GOOS { - case "linux": - exec.Command("xdg-open", cmUrl).Start() - case "windows": - exec.Command("rundll32", "url.dll,FileProtocolHandler", cmUrl).Start() - case "darwin": - exec.Command("open", cmUrl).Start() - default: - return fmt.Errorf("unsupported platform") - } - - return nil -} - -func AllFilesSortedByDate(logDirectory string) []fs.FileInfo { - files, _ := ioutil.ReadDir(logDirectory) - sort.Slice(files, func(i, j int) bool { - return files[i].ModTime().Before(files[j].ModTime()) - }) - - return files -} - -func Base64EncodeStripped(s string) string { - encoded := b64.StdEncoding.EncodeToString([]byte(s)) - return strings.TrimRight(encoded, "=") -} - -func Base64DecodeStripped(s string) (string, error) { - if i := len(s) % 4; i != 0 { - s += strings.Repeat("=", 4-i) - } - decoded, err := b64.StdEncoding.DecodeString(s) - return string(decoded), err -}