diff --git a/fsearch.go b/fsearch.go index 01fd227..165062d 100644 --- a/fsearch.go +++ b/fsearch.go @@ -36,8 +36,10 @@ func fSearch(searchTerm string, limit int) { if _, excludedName := excludedFileNames[base]; excludedName { continue // Skip this binary if its name is excluded } - entry := fmt.Sprintf("%s - %s", binary.Name, binary.Description) - searchResultsSet[entry] = struct{}{} + if binary.Description != "" { + entry := fmt.Sprintf("%s - %s", binary.Name, binary.Description) + searchResultsSet[entry] = struct{}{} + } } } @@ -66,21 +68,15 @@ func fSearch(searchTerm string, limit int) { description := parts[1] installPath := filepath.Join(InstallDir, name) - cachedLocation, _ := ReturnCachedFile(name) - var prefix string + prefix := "[-]" if fileExists(installPath) { prefix = "[i]" - } else { - binaryPath, _ := exec.LookPath(name) // is it okay to ignore the err channel of LookPath? - if binaryPath != "" { - prefix = "[\033[4mi\033[0m]" // Print [i], I is underlined. - } else if cachedLocation != "" && isExecutable(cachedLocation) { - prefix = "[c]" - } else { - prefix = "[-]" - } + } else if path, err := exec.LookPath(name); err == nil && path != "" { + prefix = "[\033[4mi\033[0m]" // Print [i],'i' is underlined + } else if cachedLocation != "" && isExecutable(cachedLocation) { + prefix = "[c]" } truncatePrintf("%s %s - %s ", prefix, name, description) diff --git a/go.mod b/go.mod index f36abb8..fee2f0c 100644 --- a/go.mod +++ b/go.mod @@ -2,11 +2,14 @@ module github.com/xplshn/bigdl go 1.22.0 -require github.com/schollz/progressbar/v3 v3.14.3 +require ( + github.com/goccy/go-json v0.10.3 + github.com/schollz/progressbar/v3 v3.14.4 +) require ( github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/rivo/uniseg v0.4.7 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/term v0.20.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 // indirect ) diff --git a/go.sum b/go.sum index 29ef316..2a431b8 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= @@ -9,13 +11,15 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/schollz/progressbar/v3 v3.14.3 h1:oOuWW19ka12wxYU1XblR4n16wF/2Y1dBLMarMo6p4xU= -github.com/schollz/progressbar/v3 v3.14.3/go.mod h1:aT3UQ7yGm+2ZjeXPqsjTenwL3ddUiuZ0kfQ/2tHlyNI= +github.com/schollz/progressbar/v3 v3.14.4 h1:W9ZrDSJk7eqmQhd3uxFNNcTr0QL+xuGNI9dEMrw0r74= +github.com/schollz/progressbar/v3 v3.14.4/go.mod h1:aT3UQ7yGm+2ZjeXPqsjTenwL3ddUiuZ0kfQ/2tHlyNI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= diff --git a/helperFunctions.go b/helperFunctions.go index b950ba9..948b6bc 100644 --- a/helperFunctions.go +++ b/helperFunctions.go @@ -3,7 +3,6 @@ package main import ( "context" - "encoding/json" "fmt" "io" "net/http" @@ -16,13 +15,11 @@ import ( "strings" "syscall" + "github.com/goccy/go-json" "github.com/schollz/progressbar/v3" ) -// TODO: Add *PROPER* error handling in the truncate functions. Ensure escape sequences are correctly handled? - -// signalHandler sets up a channel to listen for interrupt signals and returns a function -// that can be called to check if an interrupt has been received. +// signalHandler sets up a channel to listen for interrupt signals and returns a function that can be called to check if an interrupt has been received. func signalHandler(ctx context.Context, cancel context.CancelFunc) (func() bool, error) { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) @@ -256,9 +253,10 @@ func spawnProgressBar(contentLength int64) *progressbar.ProgressBar { }), ) } - return progressbar.NewOptions( - -1, + return progressbar.NewOptions(-1, progressbar.OptionSetWriter(io.Discard), + progressbar.OptionSetVisibility(false), + progressbar.OptionShowBytes(false), ) } @@ -343,7 +341,7 @@ func truncateSprintf(format string, a ...interface{}) string { // Determine the truncation length & truncate the formatted string if it exceeds the available space availableSpace := getTerminalWidth() - len(indicator) if len(formatted) > availableSpace { - formatted = fmt.Sprintf("%s", formatted[:availableSpace]) + formatted = formatted[:availableSpace] for strings.HasSuffix(formatted, ",") || strings.HasSuffix(formatted, ".") || strings.HasSuffix(formatted, " ") { formatted = formatted[:len(formatted)-1] } @@ -353,16 +351,16 @@ func truncateSprintf(format string, a ...interface{}) string { return formatted } +// NOTE: \n will always get cut off when using a truncate function, this may also happen to other formatting options // truncatePrintf is a drop-in replacement for fmt.Printf that truncates the input string if it exceeds a certain length. func truncatePrintf(format string, a ...interface{}) (n int, err error) { if DisableTruncation { - return fmt.Print(fmt.Sprintf(format, a...)) + return fmt.Printf(format, a...) } return fmt.Print(truncateSprintf(format, a...)) -} // NOTE: Both truncate functions will remove the escape sequences of truncated lines, and sometimes break them in half because of the truncation. Avoid using escape sequences with truncate functions, as it is UNSAFE. +} // validateProgramsFrom validates programs against the files in the specified directory against the remote binaries. -// It returns the validated programs based on the last element of the received list of programs. func validateProgramsFrom(InstallDir string, programsToValidate []string) ([]string, error) { // Fetch the list of binaries from the remote source once remotePrograms, err := listBinaries() @@ -405,16 +403,5 @@ func validateProgramsFrom(InstallDir string, programsToValidate []string) ([]str // Handle the list of programs received based on the last element // If programsToValidate is not nil, handle based on the last element - if len(programsToValidate) != 0 { - lastElement := programsToValidate[len(programsToValidate)-1] - switch lastElement { - case "_2_": - return invalidPrograms, nil - case "_3_": - return append(validPrograms, invalidPrograms...), nil - default: - return validPrograms, nil - } - } return validPrograms, nil } diff --git a/info.go b/info.go index 10bdd60..d46e742 100644 --- a/info.go +++ b/info.go @@ -14,6 +14,7 @@ type BinaryInfo struct { Version string `json:"repo_version"` Updated string `json:"repo_updated"` Size string `json:"size"` + Extras string `json:"extra_bins"` SHA256 string `json:"sha256"` Source string `json:"download_url"` } @@ -23,10 +24,11 @@ func findBinaryInfo(metadata []map[string]interface{}, binaryName string) (Binar if name, ok := binMap["name"].(string); ok && name == binaryName { description, _ := binMap["description"].(string) repo, _ := binMap["repo_url"].(string) - build_date, _ := binMap["build_date"].(string) + buildDate, _ := binMap["build_date"].(string) version, _ := binMap["repo_version"].(string) updated, _ := binMap["repo_updated"].(string) size, _ := binMap["size"].(string) + extras, _ := binMap["extra_bins"].(string) sha256, _ := binMap["sha256"].(string) source, _ := binMap["download_url"].(string) @@ -34,10 +36,11 @@ func findBinaryInfo(metadata []map[string]interface{}, binaryName string) (Binar Name: name, Description: description, Repo: repo, - ModTime: build_date, + ModTime: buildDate, Version: version, Updated: updated, Size: size, + Extras: extras, SHA256: sha256, Source: source, }, true diff --git a/install.go b/install.go index 37e085e..9ae9c65 100644 --- a/install.go +++ b/install.go @@ -50,11 +50,15 @@ func installCommand(silent bool, binaryNames string) error { } if err := fetchBinaryFromURL(url, installPath); err != nil { - return fmt.Errorf("error: Could not install binary: %v", err) + return fmt.Errorf("%v", err) } if !silent { - fmt.Printf("Installed: %s\n", installPath) + if InstallMessage != "disabled" { + fmt.Print(InstallMessage) + } else { + fmt.Printf("Succesfully created %s\n", installPath) + } } } return nil diff --git a/listBinaries.go b/listBinaries.go index 0ec35bc..5893830 100644 --- a/listBinaries.go +++ b/listBinaries.go @@ -2,75 +2,72 @@ package main import ( - "encoding/json" "fmt" - "io" - "net/http" "path/filepath" - "sort" "strings" ) -// listBinariesCommand fetches and lists binary names from the given URL. +// listBinaries fetches and lists binary names from the given URL. func listBinaries() ([]string, error) { - var allBinaries []string + var allBinaries []struct { + Name string `json:"Name"` + NameAlt string `json:"name"` + SHA256 string `json:"sha256"` + SHA string `json:"sha"` + } // Fetch binaries from each metadata URL for _, url := range MetadataURLs { - // Fetch metadata from the given URL - resp, err := http.Get(url) - if err != nil { - return nil, fmt.Errorf("error fetching metadata from %s: %v", url, err) + var metadata []struct { + Name string `json:"Name"` + NameAlt string `json:"name"` + SHA256 string `json:"sha256"` + SHA string `json:"sha"` } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("failed to fetch metadata from %s. HTTP status code: %d", url, resp.StatusCode) + // Use fetchJSON to fetch and unmarshal the JSON data + if err := fetchJSON(url, &metadata); err != nil { + return nil, fmt.Errorf("failed to fetch metadata from %s: %v", url, err) } - // Read response body - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %v", err) - } + // Extract binary names and SHA values + allBinaries = append(allBinaries, metadata...) + } - // Unmarshal JSON - var metadata []struct { - Name string `json:"Name"` // Consider both "name" and "Name" fields - NameAlt string `json:"name"` - } - if err := json.Unmarshal(body, &metadata); err != nil { - return nil, fmt.Errorf("failed to unmarshal metadata JSON from %s: %v", url, err) + // Filter out excluded file types and file names, and build a map of SHA256 and SHA values to binary names + filteredBinaries := make(map[string]string) + shaMap := make(map[string]bool) + for _, item := range allBinaries { + binary := item.Name + if binary == "" { + binary = item.NameAlt } - // Extract binary names - for _, item := range metadata { - if item.Name != "" { - allBinaries = append(allBinaries, item.Name) - } - if item.NameAlt != "" { - allBinaries = append(allBinaries, item.NameAlt) + if binary != "" { + ext := strings.ToLower(filepath.Ext(binary)) + base := filepath.Base(binary) + if _, excluded := excludedFileTypes[ext]; !excluded { + if _, excludedName := excludedFileNames[base]; !excludedName { + if _, exists := shaMap[item.SHA256]; !exists { + shaMap[item.SHA256] = true + filteredBinaries[binary] = item.SHA256 + } + if _, exists := shaMap[item.SHA]; !exists { + shaMap[item.SHA] = true + filteredBinaries[binary] = item.SHA + } + } } } } - // Filter out excluded file types and file names - var filteredBinaries []string - for _, binary := range allBinaries { - ext := strings.ToLower(filepath.Ext(binary)) - base := filepath.Base(binary) - if _, excluded := excludedFileTypes[ext]; !excluded { - if _, excludedName := excludedFileNames[base]; !excludedName { - filteredBinaries = append(filteredBinaries, binary) - } - } + // Define and fill uniqueBinaries + var uniqueBinaries []string + for binary := range filteredBinaries { + uniqueBinaries = append(uniqueBinaries, binary) } - - // Remove duplicates - uniqueBinaries := removeDuplicates(filteredBinaries) - - // Sort binaries alphabetically - sort.Strings(uniqueBinaries) + // Remove duplicates (entries with same name) + uniqueBinaries = removeDuplicates(uniqueBinaries) // Return the list of binaries return uniqueBinaries, nil diff --git a/main.go b/main.go index 0ade8b6..03bb7c0 100644 --- a/main.go +++ b/main.go @@ -22,7 +22,9 @@ var ( // InstallDir holds the directory that shall be used for installing, removing, updating, listing with `info`. It takes the value of $INSTALL_DIR if it is set in the user's env, otherwise it is set to have a default value InstallDir = os.Getenv("INSTALL_DIR") // TEMPDIR will be used as the dir to download files to before moving them to a final destination AND as the place that will hold cached binaries downloaded by `run` - TEMPDIR = os.Getenv("BIGDL_CACHEDIR") // Will default to "/tmp/bigdl_cached" if $TMPDIR is not set + TEMPDIR = os.Getenv("BIGDL_CACHEDIR") + // InstallMessage will be printed when installCommand() succeeds + InstallMessage = "disabled" // InstallUseCache determines if cached files should be used when requesting an install InstallUseCache = true // UseProgressBar determines if the progressbar is shown or not @@ -32,7 +34,7 @@ var ( ) const ( - VERSION = "1.6.7" // VERSION to be displayed + VERSION = "1.6.8" // VERSION to be displayed usagePage = " [-v|-h] [list|install|remove|update|run|info|search|tldr] <-args->" // usagePage to be shown // Truncation indicator indicator = "...>" @@ -80,7 +82,6 @@ func init() { } InstallDir = filepath.Join(homeDir, ".local", "bin") } - if TEMPDIR == "" { cacheDir, err := os.UserCacheDir() if err != nil { @@ -181,7 +182,6 @@ func main() { errorOut(" bigdl:%s\n", usagePage) } - // From now on, things happen. if err := os.MkdirAll(InstallDir, os.ModePerm); err != nil { fmt.Fprintf(os.Stderr, "Error: Failed to get user's Home directory. %v\n", err) os.Exit(1) @@ -198,7 +198,8 @@ func main() { case "list": if len(os.Args) == 3 { if os.Args[2] == "--described" || os.Args[2] == "-d" { - fSearch("", 99999) // Call fSearch with an empty query and a large limit to list all described binaries + // Call fSearch with an empty query and a large limit to list all described binaries + fSearch("", 99999) } else { errorOut("bigdl: Unknown command.\n") } @@ -279,7 +280,7 @@ func main() { if binaryInfo.Size != "" { fmt.Printf("Size: %s\n", binaryInfo.Size) } - if binaryInfo.Source != "" { + if binaryInfo.Source != "" { // if binaryInfo.Extras != "" { fmt.Printf("Source: %s\n", binaryInfo.Source) } if binaryInfo.SHA256 != "" { diff --git a/run.go b/run.go index 0dcae5a..2871d44 100644 --- a/run.go +++ b/run.go @@ -20,15 +20,11 @@ var ( // ReturnCachedFile retrieves the cached file location. Returns an empty string and error code 1 if not found. func ReturnCachedFile(binaryName string) (string, int) { - // Construct the expected cached file pattern - expectedCachedFile := filepath.Join(TEMPDIR, fmt.Sprintf("%s.bin", binaryName)) - - // Check if the file exists using the fileExists function - if fileExists(expectedCachedFile) { - return expectedCachedFile, 0 + cachedBinary := filepath.Join(TEMPDIR, binaryName) + if fileExists(cachedBinary) { + return cachedBinary, 0 } - - // If the file does not exist, return an error + // The file does not exist, return an error return "", 1 } @@ -80,12 +76,11 @@ func RunFromCache(binaryName string, args []string) { } if binaryName == "" { - errorOut("Error: Binary name not provided\n") + errorOut("error: Binary name not provided\n") } - // Use the base name of binaryName for constructing the cachedFile path // This way we can support requests like: toybox/wget - baseName := filepath.Base(binaryName) - cachedFile := filepath.Join(TEMPDIR, fmt.Sprintf("%s.bin", baseName)) + // Use the base name of binaryName to construc the cachedFile path. This way requests like toybox/wget are supported + cachedFile := filepath.Join(TEMPDIR, filepath.Base(binaryName)) if fileExists(cachedFile) && isExecutable(cachedFile) { if !silentMode { @@ -97,84 +92,32 @@ func RunFromCache(binaryName string, args []string) { if verboseMode { fmt.Printf("Couldn't find '%s' in the cache. Fetching a new one...\n", binaryName) } - err := fetchBinary(binaryName) - if err != nil { - fmt.Fprintf(os.Stderr, "Error fetching binary for '%s'\n", binaryName) - errorOut("Error: %s\n", err) + InstallDir = TEMPDIR + InstallMessage = "" + if err := installCommand(silentMode, binaryName); err != nil { + errorOut("%v\n", err) } cleanCache() runBinary(cachedFile, args, verboseMode) } } -// runBinary executes the binary with the given arguments, handling .bin files as needed. +// runBinary executes the binary with the given arguments. func runBinary(binaryPath string, args []string, verboseMode bool) { - programExitCode := 1 - executeBinary := func(rbinaryPath string, args []string, verboseMode bool) { - cmd := exec.Command(rbinaryPath, args...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - if err := cmd.Run(); err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - if status, ok := exitError.Sys().(syscall.WaitStatus); ok { - if verboseMode { - fmt.Printf("The program (%s) errored out with a non-zero exit code (%d).\n", binaryPath, status.ExitStatus()) - } - programExitCode = status.ExitStatus() - } - } else { - programExitCode = 1 - } - } else { - programExitCode = 0 - } - } - // If the file ends with .bin, remove the suffix and proceed to run it, afterwards, set the same suffix again. - if strings.HasSuffix(binaryPath, ".bin") { - tempFile := filepath.Join(filepath.Dir(binaryPath), strings.TrimSuffix(filepath.Base(binaryPath), ".bin")) - if err := copyFile(binaryPath, tempFile); err != nil { - fmt.Printf("failed to move binary to temporary location: %v\n", err) - return - } - if err := os.Chmod(tempFile, 0o755); err != nil { - fmt.Printf("failed to set executable bit: %v\n", err) - return - } + // Set the Controls for the Heart of the Sun + cmd := exec.Command(binaryPath, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin - executeBinary(tempFile, args, verboseMode) - if err := os.Rename(tempFile, binaryPath); err != nil { - fmt.Printf("failed to move temporary binary back to original name: %v\n", err) - return - } - } else { - executeBinary(binaryPath, args, verboseMode) - } - // Exit the program with the exit code from the executed binary or 1 if we couldn't even get to the execution - os.Exit(programExitCode) -} + err := cmd.Run() + exitCode := cmd.ProcessState.ExitCode() -// fetchBinary downloads the binary and caches it. -func fetchBinary(binaryName string) error { - url, err := findURL(binaryName) - if err != nil { - return err - } - - // Extract the base name from the binaryName to use for the cached file name - baseName := filepath.Base(binaryName) - - // Construct the cachedFile path using the base name - cachedFile := filepath.Join(TEMPDIR, baseName+".bin") - - // Fetch the binary from the repos and save it to the cache - err = fetchBinaryFromURL(url, cachedFile) - if err != nil { - return fmt.Errorf("error fetching binary for %s: %v", binaryName, err) + if err != nil && verboseMode { + fmt.Printf("The program (%s) errored out with a non-zero exit code (%d).\n", binaryPath, exitCode) } - cleanCache() - return nil + os.Exit(exitCode) } // cleanCache removes the oldest binaries when the cache size exceeds MaxCacheSize. diff --git a/update.go b/update.go index 2184c47..4acd396 100644 --- a/update.go +++ b/update.go @@ -90,8 +90,10 @@ func update(programsToUpdate []string) error { return } + // Start update process + truncatePrintf("\033[2K\r<%d/%d> %s | Looking for differences in %s against the repo's...", atomic.LoadUint32(&checked), toBeChecked, padding, program) if checkDifferences(localSHA256, binaryInfo.SHA256) == 1 { - truncatePrintf("\033[2K\r<%d/%d> %s | Detected a difference in %s. Updating...", atomic.LoadUint32(&checked), toBeChecked, padding, program) + truncatePrintf("\033[2K\r<%d/%d> %s | The repo's version of %s differs from yours. Updating...", atomic.LoadUint32(&checked), toBeChecked, padding, program) err := installCommand(true, program) if err != nil { progressMutex.Lock()