diff --git a/main.go b/main.go new file mode 100644 index 0000000..390b913 --- /dev/null +++ b/main.go @@ -0,0 +1,258 @@ +// main.go // This is the main entrypoint, which calls all the different functions //> +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "runtime" + "strconv" +) + +var ( + Repositories []string + MetadataURLs []string + validatedArch = [3]string{} + InstallDir = os.Getenv("INSTALL_DIR") + installUseCache = true + useProgressBar = true + disableTruncation = false +) + +const ( + RMetadataURL = "https://raw.githubusercontent.com/Azathothas/Toolpacks/main/metadata.json" // This is the file from which we extract descriptions for different binaries + RNMetadataURL = "https://bin.ajam.dev/METADATA.json" // This is the file which contains a concatenation of all metadata in the different repos, this one also contains sha256 checksums. + VERSION = "1.5" + usagePage = " [-v|-h] [list|install|remove|update|run|info|search|tldr] <{args}>" + // Truncation indicator + indicator = "...>" + // Cache size limit & handling. + MaxCacheSize = 10 + BinariesToDelete = 5 // Once the cache is filled - The programs populate the list of binaries to be removed in order of least used. + TEMP_DIR = "/tmp/bigdl_cached" +) + +func init() { + if InstallDir == "" { + homeDir, err := os.UserHomeDir() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: Failed to get user's Home directory. %v\n", err) + os.Exit(1) + } + InstallDir = filepath.Join(homeDir, ".local", "bin") + } + 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) + } + switch runtime.GOARCH { + case "amd64": + validatedArch = [3]string{"x86_64_Linux", "x86_64", "x86_64-Linux"} + case "arm64": + validatedArch = [3]string{"aarch64_arm64_Linux", "aarch64_arm64", "aarch64-Linux"} + default: + fmt.Println("Unsupported architecture:", runtime.GOARCH) + os.Exit(1) + } + arch := validatedArch[0] + Repositories = append(Repositories, "https://bin.ajam.dev/"+arch+"/") + Repositories = append(Repositories, "https://bin.ajam.dev/"+arch+"/Baseutils/") + Repositories = append(Repositories, "https://raw.githubusercontent.com/xplshn/Handyscripts/master/") + // These are used for listing the binaries themselves + MetadataURLs = append(MetadataURLs, "https://bin.ajam.dev/"+arch+"/METADATA.json") + MetadataURLs = append(MetadataURLs, "https://bin.ajam.dev/"+arch+"/Baseutils/METADATA.json") + MetadataURLs = append(MetadataURLs, "https://api.github.com/repos/xplshn/Handyscripts/contents") // You may add other repos if need be? bigdl is customizable, feel free to open a PR, ask questions, etc. + + if os.Getenv("DISABLE_TRUNCATION") == "true" || os.Getenv("DISABLE_TRUNCATION") == "1" { + disableTruncation = true + } + if os.Getenv("DISABLE_PRBAR") == "true" || os.Getenv("DISABLE_PRBAR") == "1" { + useProgressBar = false + } +} + +func printHelp() { + helpMessage := "Usage:\n" + usagePage + ` + +Options: + -h, --help Show this help message + -v, --version Show the version number + +Commands: + list List all available binaries + install, add Install a binary + remove, del Remove a binary + update Update binaries, by checking their SHA against the repo's SHA + run Run a binary + info Show information about a specific binary + search Search for a binary - (not all binaries have metadata. Use list to see all binaries) + tldr Show a brief description & usage examples for a given program/command. This is an alias equivalent to using "run" with "tlrc" as argument. + +Examples: + bigdl search editor + bigdl install micro + bigdl install lux --fancy "%s was installed to $INSTALL_DIR." --newline + bigdl install bed --fancy --truncate "%s was installed to $INSTALL_DIR." --newline + bigdl install orbiton --truncate "installed Orbiton to $INSTALL_DIR." + bigdl remove bed + bigdl remove orbiton tgpt lux + bigdl info jq + bigdl tldr gum + bigdl run --verbose curl -qsfSL "https://raw.githubusercontent.com/xplshn/bigdl/master/stubdl" | sh - + bigdl run --silent elinks -no-home "https://fatbuffalo.neocities.org/def" + bigdl run --transparent --silent micro ~/.profile + bigdl run btop + +Version: ` + VERSION + + fmt.Println(helpMessage) +} + +func main() { + + errorOutInsufficientArgs := func() { os.Exit(errorEncoder("Error: Insufficient parameters\n")) } + version := flag.Bool("v", false, "Show the version number") + versionLong := flag.Bool("version", false, "Show the version number") + + flag.Usage = printHelp + flag.Parse() + + if *version || *versionLong { + fmt.Println("bigdl", VERSION) + os.Exit(0) + } + + if flag.NArg() < 1 { + fmt.Printf(" bigdl:%s\n", usagePage) + os.Exit(1) + } + + switch flag.Arg(0) { + case "find_url": + binaryName := flag.Arg(1) + if binaryName == "" { + fmt.Println("Usage: bigdl find_url [binary]") + errorOutInsufficientArgs() + } + findURLCommand(binaryName) + 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 + } + if os.Args[2] == "--installed" || os.Args[2] == "info" { + installedPrograms, err := validateProgramsFrom(InstallDir, nil) + if err != nil { + fmt.Println("Error validating programs:", err) + return + } + for _, program := range installedPrograms { + fmt.Println(program) + } + } + } else { + binaries, err := listBinaries() + if err != nil { + fmt.Println("Error listing binaries:", err) + os.Exit(1) + } + for _, binary := range binaries { + fmt.Println(binary) + } + } + case "install", "add": + // Check if the binary name is provided + if flag.NArg() < 2 { + fmt.Printf("Usage: bigdl %s [binary] \n", flag.Arg(0)) + fmt.Println("Options:") + fmt.Println(" --fancy <--truncate> : Will replace exactly ONE '%s' with the name of the requested binary in the install message <--newline>") + fmt.Println(" --truncate: Truncates the message to fit into the terminal") + errorOutInsufficientArgs() + } + + binaryName := os.Args[2] + installMessage := os.Args[3:] + + installCommand(binaryName, installMessage...) + case "remove", "del": + if flag.NArg() < 2 { + fmt.Printf("Usage: bigdl %s [binar|y|ies]\n", flag.Arg(0)) + errorOutInsufficientArgs() + } + remove(flag.Args()[1:]) + case "run": + if flag.NArg() < 2 { + fmt.Println("Usage: bigdl run <--verbose, --silent, --transparent> [binary] ") + errorOutInsufficientArgs() + } + RunFromCache(flag.Arg(1), flag.Args()[2:]) + case "tldr": + if flag.NArg() < 2 { + fmt.Println("Usage: bigdl tldr [page]") + errorOutInsufficientArgs() + } + RunFromCache("tlrc", flag.Args()[1:]) + case "info": + binaryName := flag.Arg(1) + if binaryName == "" { + fmt.Println("Usage: bigdl info [binary]") + errorOutInsufficientArgs() + } + binaryInfo, err := getBinaryInfo(binaryName) + if err != nil { + errorOut("%v\n", err) + } + fmt.Printf("Name: %s\n", binaryInfo.Name) + if binaryInfo.Description != "" { + fmt.Printf("Description: %s\n", binaryInfo.Description) + } + if binaryInfo.Repo != "" { + fmt.Printf("Repo: %s\n", binaryInfo.Repo) + } + if binaryInfo.Size != "" { + fmt.Printf("Size: %s\n", binaryInfo.Size) + } + if binaryInfo.SHA256 != "" { + fmt.Printf("SHA256: %s\n", binaryInfo.SHA256) + } + if binaryInfo.B3SUM != "" { + fmt.Printf("B3SUM: %s\n", binaryInfo.B3SUM) + } + if binaryInfo.Source != "" { + fmt.Printf("Source: %s\n", binaryInfo.Source) + } + case "search": + limit := 90 + queryIndex := 2 + + if len(os.Args) < queryIndex+1 { + fmt.Println("Usage: bigdl search <--limit||-l [int]> [query]") + os.Exit(1) + } + + if len(os.Args) > 2 && os.Args[queryIndex] == "--limit" || os.Args[queryIndex] == "-l" { + if len(os.Args) > queryIndex+1 { + var err error + limit, err = strconv.Atoi(os.Args[queryIndex+1]) + if err != nil { + errorOut("Error: 'limit' value is not an int.\n") + } + queryIndex += 2 + } else { + errorOut("Error: Missing 'limit' value.\n") + } + } + + query := os.Args[queryIndex] + fSearch(query, limit) + case "update": + var programsToUpdate []string + if len(os.Args) > 2 { + programsToUpdate = os.Args[2:] + } + update(programsToUpdate) + default: + errorOut("bigdl: Unknown command.\n") + } +} diff --git a/update.go b/update.go new file mode 100644 index 0000000..b075479 --- /dev/null +++ b/update.go @@ -0,0 +1,166 @@ +// update.go // This file holds the implementation for the "update" functionality - (parallel) //> +package main + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" + "path/filepath" + "sync" + "sync/atomic" +) + +// update checks for updates to the valid programs and installs any that have changed. +func update(programsToUpdate []string) error { + // 'Configure' external functions + useProgressBar = false + installUseCache = false + + // Initialize counters + var ( + skipped, updated, errors, toBeChecked uint32 + checked uint32 + errorMessages string + padding = " " + ) + + // Call validateProgramsFrom with InstallDir and programsToUpdate + programsToUpdate, err := validateProgramsFrom(InstallDir, programsToUpdate) + if err != nil { + fmt.Println("Error validating programs:", err) + return err + } + + // Calculate toBeChecked + toBeChecked = uint32(len(programsToUpdate)) + + // Use a mutex for thread-safe updates to the progress + var progressMutex sync.Mutex + + // Use a wait group to wait for all programs to finish updating + var wg sync.WaitGroup + + // Iterate over programsToUpdate and download/update each one concurrently + for _, program := range programsToUpdate { + // Increment the WaitGroup counter + wg.Add(1) + + // Launch a goroutine to update the program + go func(program string) { + defer wg.Done() + + installPath := filepath.Join(InstallDir, program) + if !fileExists(installPath) { + progressMutex.Lock() + atomic.AddUint32(&checked, 1) + atomic.AddUint32(&skipped, 1) + truncatePrintf("\033[2K\r<%d/%d> %s | Warning: Tried to update a non-existent program %s. Skipping.", atomic.LoadUint32(&checked), toBeChecked, padding, program) + progressMutex.Unlock() + return + } + localSHA256, err := getLocalSHA256(installPath) + if err != nil { + progressMutex.Lock() + atomic.AddUint32(&checked, 1) + atomic.AddUint32(&skipped, 1) + truncatePrintf("\033[2K\r<%d/%d> %s | Warning: Failed to get SHA256 for %s. Skipping.", atomic.LoadUint32(&checked), toBeChecked, padding, program) + progressMutex.Unlock() + return + } + + binaryInfo, err := getBinaryInfo(program) + if err != nil { + progressMutex.Lock() + atomic.AddUint32(&checked, 1) + atomic.AddUint32(&skipped, 1) + truncatePrintf("\033[2K\r<%d/%d> %s | Warning: Failed to get metadata for %s. Skipping.", atomic.LoadUint32(&checked), toBeChecked, padding, program) + progressMutex.Unlock() + return + } + + // Skip if the SHA field is null + if binaryInfo.SHA256 == "" { + progressMutex.Lock() + atomic.AddUint32(&checked, 1) + atomic.AddUint32(&skipped, 1) + truncatePrintf("\033[2K\r<%d/%d> %s | Skipping %s because the SHA256 field is null.", atomic.LoadUint32(&checked), toBeChecked, padding, program) + progressMutex.Unlock() + return + } + + 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) + installMessage := truncateSprintf("\x1b[A\033[KUpdating %s", program) + err := installCommand(program, installMessage) + if err != nil { + progressMutex.Lock() + atomic.AddUint32(&errors, 1) + errorMessages += sanitizeString(fmt.Sprintf("Failed to update '%s', please check this file's properties, etc\n", program)) + progressMutex.Unlock() + return + } + progressMutex.Lock() + atomic.AddUint32(&checked, 1) + atomic.AddUint32(&updated, 1) + truncatePrintf("\033[2K\r<%d/%d> %s | Successfully updated %s.", atomic.LoadUint32(&checked), toBeChecked, padding, program) + progressMutex.Unlock() + } else { + progressMutex.Lock() + atomic.AddUint32(&checked, 1) + truncatePrintf("\033[2K\r<%d/%d> %s | No updates available for %s.", atomic.LoadUint32(&checked), toBeChecked, padding, program) + progressMutex.Unlock() + } + }(program) + } + + // Wait for all goroutines to finish + wg.Wait() + + // Prepare final counts + finalCounts := fmt.Sprintf("\033[2K\rSkipped: %d\tUpdated: %d\tChecked: %d", atomic.LoadUint32(&skipped), atomic.LoadUint32(&updated), uint32(int(atomic.LoadUint32(&checked)))) + if errors > 0 { + finalCounts += fmt.Sprintf("\tErrors: %d", atomic.LoadUint32(&errors)) + } + // Print final counts + fmt.Println(finalCounts) + fmt.Printf(errorMessages) + + return nil +} + +func contains(slice []string, str string) bool { + for _, v := range slice { + if v == str { + return true + } + } + return false +} + +// getLocalSHA256 calculates the SHA256 checksum of the local file. +func getLocalSHA256(filePath string) (string, error) { + // Open the file for reading + file, err := os.Open(filePath) + if err != nil { + return "", fmt.Errorf("failed to open file: %v", err) + } + defer file.Close() + + // Calculate SHA256 checksum + hasher := sha256.New() + if _, err := io.Copy(hasher, file); err != nil { + return "", fmt.Errorf("failed to calculate SHA256: %v", err) + } + sha256Checksum := hex.EncodeToString(hasher.Sum(nil)) + + return sha256Checksum, nil +} + +func checkDifferences(localSHA256, remoteSHA256 string) int { + if localSHA256 != remoteSHA256 { + return 1 + } + return 0 +}