package main import ( "bufio" "bytes" "eon/common" "eon/lib" "fmt" "golang.org/x/term" "io" "math/big" "net/http" "net/url" "os" "regexp" "strings" "github.com/dustin/go-humanize" "github.com/fatih/color" ) var logger = &common.DefaultLogger func main() { if len(os.Args) < 2 { fmt.Println("Usage: eon [args]") os.Exit(1) } switch os.Args[1] { case "help": if len(os.Args) < 3 { fmt.Println("Usage: eon help (list/info/install/remove/clean/repo/help) [args]") os.Exit(1) } else { switch os.Args[2] { case "list": fmt.Println("Usage: eon list (remote/local)") fmt.Println("- remote: lists packages available in the repositories.") fmt.Println("- local: lists installed packages.") case "info": fmt.Println("Usage: eon info ") fmt.Println("Shows information about a package.") case "install": fmt.Println("Usage: eon install []") fmt.Println("Installs a package.") case "remove": fmt.Println("Usage: eon remove ") fmt.Println("Removes a package.") case "clean": fmt.Println("Usage: eon clean") fmt.Println("Removes unused dependencies.") case "repo": fmt.Println("Usage: eon repo (add/remove/list)") fmt.Println("- add : adds a repository.") fmt.Println("- remove : removes a repository.") fmt.Println("- list: lists repositories.") case "help": fmt.Println("Usage: eon help (list/info/install/remove/clean/repo/help) [args]") fmt.Println("Shows help.") default: fmt.Println(color.RedString("Unknown command: " + os.Args[2])) os.Exit(1) } } case "install": var forceMode bool var inMemoryMode bool if len(os.Args) < 3 { fmt.Println("Usage: eon install ") os.Exit(1) } var localPackageList []string fileInfos := make(map[string]os.FileInfo) var repoPackageList []string for _, pkg := range os.Args[2:] { if !strings.HasPrefix(pkg, "-") { fileInfo, err := os.Stat(pkg) if err != nil { if os.IsNotExist(err) { // This unholy regex is a package name validator. It validates repository-name/package-name/1.0.0-prerelease.1+meta.1 // The regex is so unholy it breaks JetBrains' regex parser, so I had to disable it. //goland:noinspection RegExpUnnecessaryNonCapturingGroup match, err := regexp.Match(`^(?:([a-z0-9_-]+)/)?([a-z0-9_-]+)(?:/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)?$`, []byte(pkg)) if err != nil { fmt.Println(color.RedString("Failed to validate package name: " + err.Error())) os.Exit(1) } if !match { fmt.Println(color.RedString("Invalid package name: " + pkg)) os.Exit(1) } else { repoPackageList = append(repoPackageList, pkg) } } } else { localPackageList = append(localPackageList, pkg) fileInfos[pkg] = fileInfo } } else { switch pkg { case "--force", "-f": forceMode = true case "--optimizeForSpeed", "-O": inMemoryMode = true } } } // We contact the database before we print anything as to not break the formatting. err := common.EstablishDBConnection(logger) if err != nil { fmt.Println(color.RedString("Failed to establish a connection to the database: " + err.Error())) os.Exit(1) } // We refresh the package list. This takes a lot of arguments, haha. err = common.RefreshPackageList(common.DefaultListRepositoriesInDB, common.DefaultAddRemotePackageToDB, common.DefaultGetFingerprintFromDB, common.DefaultAddFingerprintToDB, common.DefaultCheckRepositoryInDB, common.DefaultAddRepositoryToDB, common.DefaultListRemotePackagesInDB, common.DefaultRemoveRemotePackageFromDB, logger) if err != nil { fmt.Println(color.RedString("Failed to refresh package list: " + err.Error())) os.Exit(1) } // Let's start printing things. if len(localPackageList) > 0 || len(repoPackageList) > 0 { var epkList []common.InstallPackage var skipEpkList [][]string var dependencies int for _, pkg := range localPackageList { epkFile, err := os.Open(pkg) if err != nil { fmt.Println(color.RedString("Failed to open package: " + err.Error())) os.Exit(1) } var displayData lib.EPKPreMap var vagueErr error var epkBytes bytes.Buffer if !inMemoryMode { displayData, err, vagueErr = lib.PreMapEPK(lib.StreamOrBytes{FileStream: epkFile, IsFileStream: true}, fileInfos[pkg].Size()) } else { _, err = io.Copy(bufio.NewWriter(&epkBytes), epkFile) if err != nil { fmt.Println(color.RedString("Failed to read package into memory: " + err.Error() + ", have you tried disabling in-memory mode?")) os.Exit(1) } err := epkFile.Close() if err != nil { fmt.Println(color.HiYellowString("Failed to close package file: " + err.Error() + ", memory leak possible.")) } displayData, err, vagueErr = lib.PreMapEPK(lib.StreamOrBytes{Bytes: epkBytes.Bytes()}, fileInfos[pkg].Size()) } if err != nil || vagueErr != nil { common.PreMapEPKHandleError(err, vagueErr, logger) os.Exit(1) } var installPackage common.InstallPackage installPackage.EPKPreMap = &displayData installPackage.Url = "" installPackage.Priority = 0 installPackage.IsRemote = false installPackage.IsForced = false if !inMemoryMode { installPackage.StreamOrBytes = lib.StreamOrBytes{ FileStream: epkFile, IsFileStream: true, } } else { installPackage.StreamOrBytes = lib.StreamOrBytes{ Bytes: epkBytes.Bytes(), } } installPackage.Repository = lib.Repository{Name: "Local file"} displayData.IsUpgrade = true version, exists, err := common.DefaultCheckEPKInDB(displayData.Name) if err != nil { fmt.Println(color.RedString("Failed to check for package in database: " + err.Error())) } if exists { if version.Compare(&displayData.Version) != -1 { if !forceMode { skipEpk := []string{displayData.Name, displayData.Architecture, displayData.Version.String(), "Local file", humanize.BigIBytes(displayData.DecompressedSize), "Skip"} skipEpkList = append(skipEpkList, skipEpk) } else { installPackage.IsForced = true addedDeps, err := common.HandleDependencies(dependencies, installPackage, 0, []lib.RemoteEPK{}, common.DefaultListRemotePackagesInDB, &epkList, logger) if err != nil { fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error())) os.Exit(1) } else { dependencies += addedDeps } epkList = append(epkList, installPackage) } } else { addedDeps, err := common.HandleDependencies(dependencies, installPackage, 0, []lib.RemoteEPK{}, common.DefaultListRemotePackagesInDB, &epkList, logger) if err != nil { fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error())) os.Exit(1) } else { dependencies += addedDeps } epkList = append(epkList, installPackage) } } else { displayData.IsUpgrade = false addedDeps, err := common.HandleDependencies(dependencies, installPackage, 0, []lib.RemoteEPK{}, common.DefaultListRemotePackagesInDB, &epkList, logger) if err != nil { fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error())) os.Exit(1) } else { dependencies += addedDeps } epkList = append(epkList, installPackage) } } for _, pkg := range repoPackageList { // Check if the package is already installed. version, exists, err := common.DefaultCheckEPKInDB(pkg) if err != nil { fmt.Println(color.RedString("Failed to check for package in database: " + err.Error())) os.Exit(1) } var remoteEPK lib.RemoteEPK var epkEntry common.InstallPackage remoteEpkList, err := common.DefaultListRemotePackagesInDB() if err != nil { fmt.Println(color.RedString("Failed to list remote packages: " + err.Error())) os.Exit(1) } for _, epk := range remoteEpkList { if epk.Name == pkg { remoteEPK = epk break } } // Calculate the download URL epkDownloadUrl, err := url.JoinPath(remoteEPK.Repository.URL, remoteEPK.Path) if err != nil { fmt.Println(color.RedString("Failed to join URL: " + err.Error())) os.Exit(1) } // Pre-map the EPK displayData, err, vagueErr := lib.PreMapRemoteEPK(remoteEPK, logger) if err != nil || vagueErr != nil { common.PreMapEPKHandleError(err, vagueErr, logger) os.Exit(1) } // Map the data we have epkEntry.Priority = 0 epkEntry.EPKPreMap = &displayData epkEntry.Url = epkDownloadUrl epkEntry.IsRemote = true epkEntry.Repository = remoteEPK.Repository if !inMemoryMode { epkEntry.StreamOrBytes.IsURL = true epkEntry.StreamOrBytes.URL = epkDownloadUrl epkEntry.StreamOrBytes.RepositoryName = remoteEPK.Repository.Name } else { // Download the entire EPK into memory epkBytes, err := http.Get(epkDownloadUrl) if err != nil { fmt.Println(color.RedString("Failed to download package: " + err.Error())) os.Exit(1) } if epkBytes.StatusCode != 200 { fmt.Println(color.RedString("Failed to download package: " + epkBytes.Status)) os.Exit(1) } // Calculate the total size of the package contentLength := new(big.Int) contentLength.SetString(epkBytes.Header.Get("Content-Length"), 10) // Print that we are downloading the package fmt.Println("\nDownloading package: " + displayData.Name + " (" + humanize.BigIBytes(contentLength) + ")") // Hide the cursor for reasons explained below. fmt.Print("\033[?25l") // Copy the stream into a buffer var buffer bytes.Buffer _, err = io.Copy(&lib.ProgressWriter{ Logger: logger, Total: contentLength, Writer: &buffer, }, epkBytes.Body) if err != nil { fmt.Println(color.RedString("Failed to read package into memory: " + err.Error())) os.Exit(1) } // Set the progress to 100% logger.LogFunc(lib.Log{ Level: "PROGRESS", Progress: big.NewInt(1), Total: big.NewInt(1), }) // Show the cursor again because we're done with the progress bar. fmt.Print("\033[?25h") // Close the response body err = epkBytes.Body.Close() if err != nil { fmt.Println(color.HiYellowString("Failed to close response body: " + err.Error() + ", memory leak possible.")) } // Set the buffer as the bytes epkEntry.StreamOrBytes.Bytes = buffer.Bytes() epkEntry.StreamOrBytes.IsURL = false epkEntry.StreamOrBytes.IsFileStream = false // Reset the buffer buffer.Reset() } // Make decisions on what happens if the package is already installed. if exists { if version.Compare(&displayData.Version) != -1 { // If the version is the same or newer, skip the package. if !forceMode { skipEpkList = append(skipEpkList, []string{displayData.Name, displayData.Architecture, displayData.Version.String(), remoteEPK.Repository.Name, humanize.BigIBytes(displayData.DecompressedSize), "Skip"}) } else { // If the version is the same or newer, but the user wants to force the installation, install it. epkEntry.IsForced = true // We can let it use our remoteEPKList to save a SQL query. addedDeps, err := common.HandleDependencies(dependencies, epkEntry, 0, remoteEpkList, common.DefaultListRemotePackagesInDB, &epkList, logger) if err != nil { fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error())) os.Exit(1) } else { // We add the dependencies to the total count. dependencies += addedDeps } epkList = append(epkList, epkEntry) } } else { // If the version is older, install it. addedDeps, err := common.HandleDependencies(dependencies, epkEntry, 0, remoteEpkList, common.DefaultListRemotePackagesInDB, &epkList, logger) if err != nil { fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error())) os.Exit(1) } else { dependencies += addedDeps } epkList = append(epkList, epkEntry) } } else { // If the package is not installed, install it. addedDeps, err := common.HandleDependencies(dependencies, epkEntry, 0, remoteEpkList, common.DefaultListRemotePackagesInDB, &epkList, logger) if err != nil { fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error())) os.Exit(1) } else { dependencies += addedDeps } epkList = append(epkList, epkEntry) } } // Give the summary of the installation. fmt.Println("\nThe following packages will be installed:") width, _, err := term.GetSize(int(os.Stdout.Fd())) if err != nil { fmt.Println(color.RedString("Failed to get terminal width: " + err.Error())) os.Exit(1) } if width < 42 { fmt.Println(color.RedString("Terminal too small. Minimum required width: 42 characters.")) os.Exit(1) } for range width { fmt.Print("=") } fmt.Println() tableList := []string{"Package", "Architecture", "Version", "Repository", "Installed Size", "Action"} maxSize := width / 6 for _, item := range tableList { common.PrintWithEvenPadding(item, maxSize) } fmt.Println() for range width { fmt.Print("=") } fmt.Println() for _, pkg := range skipEpkList { for _, item := range pkg { common.PrintWithEvenPadding(color.GreenString(item), maxSize) } } for _, pkg := range epkList { finalisedList := make([]string, 6) finalisedList[0] = pkg.EPKPreMap.Name finalisedList[1] = pkg.EPKPreMap.Architecture finalisedList[2] = pkg.EPKPreMap.Version.String() if !pkg.IsRemote { finalisedList[3] = "Local file" } else { finalisedList[3] = pkg.Repository.Name } finalisedList[4] = humanize.BigIBytes(pkg.EPKPreMap.DecompressedSize) if pkg.IsForced { finalisedList[5] = "Forced installation" } else if pkg.EPKPreMap.IsUpgrade { finalisedList[5] = "Upgrade" } else { finalisedList[5] = "Install" } for _, item := range finalisedList { common.PrintWithEvenPadding(item, maxSize) } } if len(epkList) > 0 { for range width { fmt.Print("=") } fmt.Println("Transaction Summary") fmt.Println("\nInstalling " + humanize.Comma(int64(len(epkList))) + " packages, of which " + humanize.Comma(int64(dependencies)) + " are dependencies.") fmt.Println("Total download size: " + humanize.BigIBytes(common.GetTotalSize(epkList))) fmt.Println("Total installed size: " + humanize.BigIBytes(common.GetTotalInstalledSize(epkList)) + "\n") response := logger.LogFunc(lib.Log{ Level: "INFO", Content: "Proceed with installation (y/n)?", Prompt: true, PlaySound: true, }) if strings.ToLower(response) == "y" { // We hide the cursor because it makes the progress bar look weird and other package managers hide // it during installation. For some reason, it builds suspense. Or it does with me anyway, when // a program hides the cursor, it makes me think twice than to Ctrl+C it :P // - Arzumify fmt.Print("\033[?25l") // Time to install things. for _, installPackage := range epkList { // Map the EPK metadata. metadata, err, vagueErr := lib.FullyMapMetadata(installPackage.StreamOrBytes, installPackage.EPKPreMap, common.DefaultGetFingerprintFromDB, common.DefaultAddFingerprintToDB, logger) if err != nil || vagueErr != nil { common.FullyMapMetadataHandleError(err, vagueErr, logger) } // Install the package. tempDir, err, vagueErr := lib.InstallEPK(installPackage.StreamOrBytes, metadata, installPackage.EPKPreMap, common.DefaultAddEPKToDB, logger) if err != nil || vagueErr != nil { common.InstallEPKHandleError(tempDir, err, vagueErr, logger) } // Done! fmt.Println("Installed package: " + installPackage.EPKPreMap.Name) } // We show the cursor again because we're done with the progress bar. fmt.Print("\033[?25h") } else { fmt.Println("Installation cancelled.") os.Exit(1) } } else { for range width { fmt.Print("=") } fmt.Println("No packages left to install. To force re-install / downgrade, use the --force flag, " + "though this may result in breakage.") os.Exit(0) } } else { fmt.Println("No packages to install.") os.Exit(0) } case "repo": if len(os.Args) < 3 { fmt.Println("Usage: eon repo (add/remove/list)") os.Exit(1) } switch os.Args[2] { case "add": if len(os.Args) < 4 { fmt.Println("Usage: eon repo add ") os.Exit(1) } else { err := common.EstablishDBConnection(logger) if err != nil { fmt.Println(color.RedString("Failed to establish a connection to the database: " + err.Error())) os.Exit(1) } repoName, err, vagueErr := lib.AddRepository(os.Args[3], common.DefaultAddRepositoryToDB, common.DefaultGetFingerprintFromDB, common.DefaultAddFingerprintToDB, common.DefaultAddRemotePackageToDB, common.DefaultCheckRepositoryInDB, false, logger) if err != nil || vagueErr != nil { common.AddRepositoryHandleError(err, vagueErr, logger) } else { fmt.Println("Added repository " + repoName + " to the database.") } } case "remove": if len(os.Args) < 4 { fmt.Println("Usage: eon repo remove ") os.Exit(1) } else { err := common.EstablishDBConnection(logger) if err != nil { fmt.Println(color.RedString("Failed to establish a connection to the database: " + err.Error())) os.Exit(1) } err, vagueErr := lib.RemoveRepository(os.Args[3], common.DefaultRemoveRepositoryFromDB, common.DefaultCheckRepositoryInDB, logger) if err != nil || vagueErr != nil { common.RemoveRepositoryHandleError(err, vagueErr, logger) } } case "list": err := common.EstablishDBConnection(logger) if err != nil { fmt.Println(color.RedString("Failed to establish a connection to the database: " + err.Error())) os.Exit(1) } repos, err := common.DefaultListRepositoriesInDB() if err != nil { fmt.Println(color.RedString("Failed to get repositories: " + err.Error())) os.Exit(1) } if len(repos) == 0 { fmt.Println("No repositories.") } else { fmt.Println("Repositories:") for _, repo := range repos { fmt.Println("\n" + repo.Name + ":\n" + " " + repo.Description + "\n URL: " + repo.URL + "\n Owner: " + repo.Owner) } fmt.Println() } } default: fmt.Println(color.RedString("Unknown or unimplemented command: " + os.Args[1])) os.Exit(1) } }