eon/cmd/main.go
2024-09-03 19:58:53 +01:00

551 lines
19 KiB
Go

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 <list/info/install/remove/clean/repo/help> [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 <package>")
fmt.Println("Shows information about a package.")
case "install":
fmt.Println("Usage: eon install <package> [<repository>]")
fmt.Println("Installs a package.")
case "remove":
fmt.Println("Usage: eon remove <package>")
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 <url>: adds a repository.")
fmt.Println("- remove <url>: 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 <package>")
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 <url>")
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 <name>")
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)
}
}